diff --git a/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts b/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts deleted file mode 100644 index 48e88e79f9674..0000000000000 --- a/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// This flag is used on the server side as the default setting. -// Plugin initialization does some additional integrity checks and tests if the necessary -// indices and aliases exist. Based on that the final setting will be available -// as an injectedVar on the client side and can be accessed like: -// - -export const FEATURE_ANNOTATIONS_ENABLED = true; diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index fc1cec7c16208..7262c83b6867d 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -45,7 +45,6 @@ export const ml = (kibana: any) => { category: DEFAULT_APP_CATEGORIES.analyze, }, styleSheetPaths: resolve(__dirname, 'public/application/index.scss'), - hacks: ['plugins/ml/application/hacks/toggle_app_link_in_nav'], savedObjectSchemas: { 'ml-telemetry': { isNamespaceAgnostic: true, diff --git a/x-pack/legacy/plugins/ml/public/application/app.tsx b/x-pack/legacy/plugins/ml/public/application/app.tsx index 085e395f2ebf7..24cbfbfb346dd 100644 --- a/x-pack/legacy/plugins/ml/public/application/app.tsx +++ b/x-pack/legacy/plugins/ml/public/application/app.tsx @@ -7,50 +7,78 @@ import React, { FC } from 'react'; import ReactDOM from 'react-dom'; -import 'uiExports/savedObjectTypes'; - -import 'ui/autoload/all'; - // needed to make syntax highlighting work in ace editors import 'ace'; import { AppMountParameters, CoreStart } from 'kibana/public'; -import { - IndexPatternsContract, - Plugin as DataPlugin, -} from '../../../../../../src/plugins/data/public'; -import { KibanaConfigTypeFix } from './contexts/kibana'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; + +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { setDependencyCache, clearCache } from './util/dependency_cache'; import { MlRouter } from './routing'; export interface MlDependencies extends AppMountParameters { - npData: ReturnType; - indexPatterns: IndexPatternsContract; + data: DataPublicPluginStart; + __LEGACY: { + XSRF: string; + APP_URL: string; + }; } interface AppProps { coreStart: CoreStart; - indexPatterns: IndexPatternsContract; + deps: MlDependencies; } -const App: FC = ({ coreStart, indexPatterns }) => { - const config = (coreStart.uiSettings as never) as KibanaConfigTypeFix; // TODO - make this UiSettingsClientContract, get rid of KibanaConfigTypeFix +const App: FC = ({ coreStart, deps }) => { + setDependencyCache({ + indexPatterns: deps.data.indexPatterns, + timefilter: deps.data.query.timefilter, + config: coreStart.uiSettings!, + chrome: coreStart.chrome!, + docLinks: coreStart.docLinks!, + toastNotifications: coreStart.notifications.toasts, + overlays: coreStart.overlays, + recentlyAccessed: coreStart.chrome!.recentlyAccessed, + fieldFormats: deps.data.fieldFormats, + autocomplete: deps.data.autocomplete, + basePath: coreStart.http.basePath, + savedObjectsClient: coreStart.savedObjects.client, + XSRF: deps.__LEGACY.XSRF, + APP_URL: deps.__LEGACY.APP_URL, + application: coreStart.application, + http: coreStart.http, + }); + deps.onAppLeave(actions => { + clearCache(); + return actions.default(); + }); + + const pageDeps = { + indexPatterns: deps.data.indexPatterns, + config: coreStart.uiSettings!, + setBreadcrumbs: coreStart.chrome!.setBreadcrumbs, + }; + + const services = { + appName: 'ML', + data: deps.data, + ...coreStart, + }; + const I18nContext = coreStart.i18n.Context; return ( - + + + + + ); }; -export const renderApp = ( - coreStart: CoreStart, - depsStart: object, - { element, indexPatterns }: MlDependencies -) => { - ReactDOM.render(, element); +export const renderApp = (coreStart: CoreStart, depsStart: object, deps: MlDependencies) => { + ReactDOM.render(, deps.element); - return () => ReactDOM.unmountComponentAtNode(element); + return () => ReactDOM.unmountComponentAtNode(deps.element); }; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx index 323de6d3a8dd5..2568a6f40d326 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx @@ -21,12 +21,7 @@ describe('AnnotationDescriptionList', () => { }); test('Initialization with annotation.', () => { - const wrapper = shallowWithIntl( - - ); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx index 3d98e2d66935c..cf8fd299c07d7 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx @@ -13,27 +13,24 @@ import React from 'react'; import { EuiDescriptionList } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { Annotation } from '../../../../../common/types/annotations'; import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; interface Props { annotation: Annotation; - intl: InjectedIntl; } -export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props) => { +export const AnnotationDescriptionList = ({ annotation }: Props) => { const listItems = [ { - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.jobIdTitle', + title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.jobIdTitle', { defaultMessage: 'Job ID', }), description: annotation.job_id, }, { - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.startTitle', + title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.startTitle', { defaultMessage: 'Start', }), description: formatHumanReadableDateTimeSeconds(annotation.timestamp), @@ -42,8 +39,7 @@ export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props if (annotation.end_timestamp !== undefined) { listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.endTitle', + title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.endTitle', { defaultMessage: 'End', }), description: formatHumanReadableDateTimeSeconds(annotation.end_timestamp), @@ -52,31 +48,36 @@ export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props if (annotation.create_time !== undefined && annotation.modified_time !== undefined) { listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdTitle', + title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdTitle', { defaultMessage: 'Created', }), description: formatHumanReadableDateTimeSeconds(annotation.create_time), }); listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdByTitle', - defaultMessage: 'Created by', - }), + title: i18n.translate( + 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdByTitle', + { + defaultMessage: 'Created by', + } + ), description: annotation.create_username, }); listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.lastModifiedTitle', - defaultMessage: 'Last modified', - }), + title: i18n.translate( + 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.lastModifiedTitle', + { + defaultMessage: 'Last modified', + } + ), description: formatHumanReadableDateTimeSeconds(annotation.modified_time), }); listItems.push({ - title: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.modifiedByTitle', - defaultMessage: 'Modified by', - }), + title: i18n.translate( + 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.modifiedByTitle', + { + defaultMessage: 'Modified by', + } + ), description: annotation.modified_username, }); } @@ -88,4 +89,4 @@ export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props listItems={listItems} /> ); -}); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx index 6668518822710..65fe36a7b611b 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx @@ -27,7 +27,6 @@ import { CommonProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; import { ANNOTATION_MAX_LENGTH_CHARS } from '../../../../../common/constants/annotations'; import { annotation$, @@ -38,6 +37,7 @@ import { AnnotationDescriptionList } from '../annotation_description_list'; import { DeleteAnnotationModal } from '../delete_annotation_modal'; import { ml } from '../../../services/ml_api_service'; +import { getToastNotifications } from '../../../util/dependency_cache'; interface Props { annotation: AnnotationState; @@ -47,7 +47,7 @@ interface State { isDeleteModalVisible: boolean; } -class AnnotationFlyoutIntl extends Component { +class AnnotationFlyoutUI extends Component { public state: State = { isDeleteModalVisible: false, }; @@ -75,6 +75,7 @@ class AnnotationFlyoutIntl extends Component { public deleteHandler = async () => { const { annotation } = this.props; + const toastNotifications = getToastNotifications(); if (annotation === null) { return; @@ -161,6 +162,7 @@ class AnnotationFlyoutIntl extends Component { .indexAnnotation(annotation) .then(() => { annotationsRefreshed(); + const toastNotifications = getToastNotifications(); if (typeof annotation._id === 'undefined') { toastNotifications.addSuccess( i18n.translate( @@ -184,6 +186,7 @@ class AnnotationFlyoutIntl extends Component { } }) .catch(resp => { + const toastNotifications = getToastNotifications(); if (typeof annotation._id === 'undefined') { toastNotifications.addDanger( i18n.translate( @@ -343,5 +346,5 @@ export const AnnotationFlyout: FC = props => { return null; } - return ; + return ; }; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 3329bf1aab64a..d9c32be41cd72 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -27,6 +27,8 @@ import { EuiLoadingSpinner, EuiToolTip, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; @@ -48,458 +50,439 @@ import { annotationsRefreshed, } from '../../../services/annotations_service'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; - const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; /** * Table component for rendering the lists of annotations for an ML job. */ -const AnnotationsTable = injectI18n( - class AnnotationsTable extends Component { - static propTypes = { - annotations: PropTypes.array, - jobs: PropTypes.array, - isSingleMetricViewerLinkVisible: PropTypes.bool, - isNumberBadgeVisible: PropTypes.bool, +export class AnnotationsTable extends Component { + static propTypes = { + annotations: PropTypes.array, + jobs: PropTypes.array, + isSingleMetricViewerLinkVisible: PropTypes.bool, + isNumberBadgeVisible: PropTypes.bool, + }; + + constructor(props) { + super(props); + this.state = { + annotations: [], + isLoading: false, + jobId: + Array.isArray(this.props.jobs) && + this.props.jobs.length > 0 && + this.props.jobs[0] !== undefined + ? this.props.jobs[0].job_id + : undefined, }; + } - constructor(props) { - super(props); - this.state = { - annotations: [], - isLoading: false, - // Need to do a detailed check here because the angular wrapper could pass on something like `[undefined]`. - jobId: - Array.isArray(this.props.jobs) && - this.props.jobs.length > 0 && - this.props.jobs[0] !== undefined - ? this.props.jobs[0].job_id - : undefined, - }; - } + getAnnotations() { + const job = this.props.jobs[0]; + const dataCounts = job.data_counts; - getAnnotations() { - const job = this.props.jobs[0]; - const dataCounts = job.data_counts; + this.setState({ + isLoading: true, + }); - this.setState({ - isLoading: true, - }); - - if (dataCounts.processed_record_count > 0) { - // Load annotations for the selected job. - ml.annotations - .getAnnotations({ - jobIds: [job.job_id], - earliestMs: null, - latestMs: null, - maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, - }) - .toPromise() - .then(resp => { - this.setState((prevState, props) => ({ - annotations: resp.annotations[props.jobs[0].job_id] || [], - errorMessage: undefined, - isLoading: false, - jobId: props.jobs[0].job_id, - })); - }) - .catch(resp => { - console.log('Error loading list of annotations for jobs list:', resp); - this.setState({ - annotations: [], - errorMessage: 'Error loading the list of annotations for this job', - isLoading: false, - jobId: undefined, - }); + if (dataCounts.processed_record_count > 0) { + // Load annotations for the selected job. + ml.annotations + .getAnnotations({ + jobIds: [job.job_id], + earliestMs: null, + latestMs: null, + maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + }) + .toPromise() + .then(resp => { + this.setState((prevState, props) => ({ + annotations: resp.annotations[props.jobs[0].job_id] || [], + errorMessage: undefined, + isLoading: false, + jobId: props.jobs[0].job_id, + })); + }) + .catch(resp => { + console.log('Error loading list of annotations for jobs list:', resp); + this.setState({ + annotations: [], + errorMessage: 'Error loading the list of annotations for this job', + isLoading: false, + jobId: undefined, }); - } + }); } + } - getJob(jobId) { - // check if the job was supplied via props and matches the supplied jobId - if (Array.isArray(this.props.jobs) && this.props.jobs.length > 0) { - const job = this.props.jobs[0]; - if (jobId === undefined || job.job_id === jobId) { - return job; - } + getJob(jobId) { + // check if the job was supplied via props and matches the supplied jobId + if (Array.isArray(this.props.jobs) && this.props.jobs.length > 0) { + const job = this.props.jobs[0]; + if (jobId === undefined || job.job_id === jobId) { + return job; } - - return mlJobService.getJob(jobId); } - annotationsRefreshSubscription = null; + return mlJobService.getJob(jobId); + } - componentDidMount() { - if ( - this.props.annotations === undefined && - Array.isArray(this.props.jobs) && - this.props.jobs.length > 0 - ) { - this.annotationsRefreshSubscription = annotationsRefresh$.subscribe(() => - this.getAnnotations() - ); - annotationsRefreshed(); - } + annotationsRefreshSubscription = null; + + componentDidMount() { + if ( + this.props.annotations === undefined && + Array.isArray(this.props.jobs) && + this.props.jobs.length > 0 + ) { + this.annotationsRefreshSubscription = annotationsRefresh$.subscribe(() => + this.getAnnotations() + ); + annotationsRefreshed(); } + } - previousJobId = undefined; - componentDidUpdate() { - if ( - Array.isArray(this.props.jobs) && - this.props.jobs.length > 0 && - this.previousJobId !== this.props.jobs[0].job_id && - this.props.annotations === undefined && - this.state.isLoading === false && - this.state.jobId !== this.props.jobs[0].job_id - ) { - annotationsRefreshed(); - this.previousJobId = this.props.jobs[0].job_id; - } + previousJobId = undefined; + componentDidUpdate() { + if ( + Array.isArray(this.props.jobs) && + this.props.jobs.length > 0 && + this.previousJobId !== this.props.jobs[0].job_id && + this.props.annotations === undefined && + this.state.isLoading === false && + this.state.jobId !== this.props.jobs[0].job_id + ) { + annotationsRefreshed(); + this.previousJobId = this.props.jobs[0].job_id; } + } - componentWillUnmount() { - if (this.annotationsRefreshSubscription !== null) { - this.annotationsRefreshSubscription.unsubscribe(); - } + componentWillUnmount() { + if (this.annotationsRefreshSubscription !== null) { + this.annotationsRefreshSubscription.unsubscribe(); } + } - openSingleMetricView = (annotation = {}) => { - // 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); - const dataCounts = job.data_counts; - const resultLatest = getLatestDataOrBucketTimestamp( - dataCounts.latest_record_timestamp, - dataCounts.latest_bucket_timestamp - ); - const from = new Date(dataCounts.earliest_record_timestamp).toISOString(); - const to = new Date(resultLatest).toISOString(); + openSingleMetricView = (annotation = {}) => { + // 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); + const dataCounts = job.data_counts; + const resultLatest = getLatestDataOrBucketTimestamp( + dataCounts.latest_record_timestamp, + dataCounts.latest_bucket_timestamp + ); + 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 globalSettings = { - ml: { - jobIds: [job.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, + const appState = { + query: { + query_string: { + analyze_wildcard: true, + query: '*', }, - time: { - from, - to, - mode: 'absolute', - }, - }; + }, + }; - const appState = { - query: { - query_string: { - analyze_wildcard: true, - query: '*', - }, + if (annotation.timestamp !== undefined && annotation.end_timestamp !== undefined) { + appState.mlTimeSeriesExplorer = { + zoom: { + from: new Date(annotation.timestamp).toISOString(), + to: new Date(annotation.end_timestamp).toISOString(), }, }; - if (annotation.timestamp !== undefined && annotation.end_timestamp !== undefined) { - appState.mlTimeSeriesExplorer = { - zoom: { - from: new Date(annotation.timestamp).toISOString(), - to: new Date(annotation.end_timestamp).toISOString(), - }, - }; - - if (annotation.timestamp < dataCounts.earliest_record_timestamp) { - globalSettings.time.from = new Date(annotation.timestamp).toISOString(); - } - - if (annotation.end_timestamp > dataCounts.latest_record_timestamp) { - globalSettings.time.to = new Date(annotation.end_timestamp).toISOString(); - } + if (annotation.timestamp < dataCounts.earliest_record_timestamp) { + globalSettings.time.from = new Date(annotation.timestamp).toISOString(); } - const _g = rison.encode(globalSettings); - const _a = rison.encode(appState); + if (annotation.end_timestamp > dataCounts.latest_record_timestamp) { + globalSettings.time.to = new Date(annotation.end_timestamp).toISOString(); + } + } - const url = `?_g=${_g}&_a=${_a}`; - addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url); - window.open(`#/timeseriesexplorer${url}`, '_self'); - }; + const _g = rison.encode(globalSettings); + const _a = rison.encode(appState); - onMouseOverRow = record => { - if (this.mouseOverRecord !== undefined) { - if (this.mouseOverRecord.rowId !== record.rowId) { - // Mouse is over a different row, fire mouseleave on the previous record. - mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' }); - - // fire mouseenter on the new record. - mlTableService.rowMouseenter$.next({ record, type: 'annotation' }); - } - } else { - // Mouse is now over a row, fire mouseenter on the record. + const url = `?_g=${_g}&_a=${_a}`; + addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url); + window.open(`#/timeseriesexplorer${url}`, '_self'); + }; + + onMouseOverRow = record => { + if (this.mouseOverRecord !== undefined) { + if (this.mouseOverRecord.rowId !== record.rowId) { + // Mouse is over a different row, fire mouseleave on the previous record. + mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' }); + + // fire mouseenter on the new record. mlTableService.rowMouseenter$.next({ record, type: 'annotation' }); } + } else { + // Mouse is now over a row, fire mouseenter on the record. + mlTableService.rowMouseenter$.next({ record, type: 'annotation' }); + } - this.mouseOverRecord = record; - }; + this.mouseOverRecord = record; + }; - onMouseLeaveRow = () => { - if (this.mouseOverRecord !== undefined) { - mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' }); - this.mouseOverRecord = undefined; - } - }; + onMouseLeaveRow = () => { + if (this.mouseOverRecord !== undefined) { + mlTableService.rowMouseleave$.next({ record: this.mouseOverRecord, type: 'annotation' }); + this.mouseOverRecord = undefined; + } + }; - render() { - const { - isSingleMetricViewerLinkVisible = true, - isNumberBadgeVisible = false, - intl, - } = this.props; + render() { + const { isSingleMetricViewerLinkVisible = true, isNumberBadgeVisible = false } = this.props; - if (this.props.annotations === undefined) { - if (this.state.isLoading === true) { - return ( - - - - - - ); - } + if (this.props.annotations === undefined) { + if (this.state.isLoading === true) { + return ( + + + + + + ); + } - if (this.state.errorMessage !== undefined) { - return ; - } + if (this.state.errorMessage !== undefined) { + return ; } + } - const annotations = this.props.annotations || this.state.annotations; + const annotations = this.props.annotations || this.state.annotations; - if (annotations.length === 0) { - return ( - + } + iconType="iInCircle" + role="alert" + > + {this.state.jobId && isTimeSeriesViewJob(this.getJob(this.state.jobId)) && ( +

this.openSingleMetricView()}> + + + ), + }} /> - } - iconType="iInCircle" - role="alert" - > - {this.state.jobId && isTimeSeriesViewJob(this.getJob(this.state.jobId)) && ( -

- this.openSingleMetricView()}> - - - ), - }} - /> -

- )} -
- ); - } +

+ )} +
+ ); + } - function renderDate(date) { - return formatDate(date, TIME_FORMAT); - } + function renderDate(date) { + return formatDate(date, TIME_FORMAT); + } - const columns = [ - { - field: 'annotation', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.annotationColumnName', - defaultMessage: 'Annotation', - }), - sortable: true, - width: '50%', - scope: 'row', - }, - { - field: 'timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.fromColumnName', - defaultMessage: 'From', - }), - dataType: 'date', - render: renderDate, - sortable: true, - }, - { - field: 'end_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.toColumnName', - defaultMessage: 'To', - }), - dataType: 'date', - render: renderDate, - sortable: true, - }, - { - field: 'modified_time', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.lastModifiedDateColumnName', - defaultMessage: 'Last modified date', - }), - dataType: 'date', - render: renderDate, - sortable: true, - }, - { - field: 'modified_username', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.lastModifiedByColumnName', - defaultMessage: 'Last modified by', - }), - sortable: true, + const columns = [ + { + field: 'annotation', + name: i18n.translate('xpack.ml.annotationsTable.annotationColumnName', { + defaultMessage: 'Annotation', + }), + sortable: true, + width: '50%', + scope: 'row', + }, + { + field: 'timestamp', + name: i18n.translate('xpack.ml.annotationsTable.fromColumnName', { + defaultMessage: 'From', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'end_timestamp', + name: i18n.translate('xpack.ml.annotationsTable.toColumnName', { + defaultMessage: 'To', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'modified_time', + name: i18n.translate('xpack.ml.annotationsTable.lastModifiedDateColumnName', { + defaultMessage: 'Last modified date', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'modified_username', + name: i18n.translate('xpack.ml.annotationsTable.lastModifiedByColumnName', { + defaultMessage: 'Last modified by', + }), + sortable: true, + }, + ]; + + const jobIds = _.uniq(annotations.map(a => a.job_id)); + if (jobIds.length > 1) { + columns.unshift({ + field: 'job_id', + name: i18n.translate('xpack.ml.annotationsTable.jobIdColumnName', { + defaultMessage: 'job ID', + }), + sortable: true, + }); + } + + if (isNumberBadgeVisible) { + columns.unshift({ + field: 'key', + name: i18n.translate('xpack.ml.annotationsTable.labelColumnName', { + defaultMessage: 'Label', + }), + sortable: true, + width: '60px', + render: key => { + return {key}; }, - ]; - - const jobIds = _.uniq(annotations.map(a => a.job_id)); - if (jobIds.length > 1) { - columns.unshift({ - field: 'job_id', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.jobIdColumnName', - defaultMessage: 'job ID', - }), - sortable: true, - }); - } + }); + } - if (isNumberBadgeVisible) { - columns.unshift({ - field: 'key', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.labelColumnName', - defaultMessage: 'Label', - }), - sortable: true, - width: '60px', - render: key => { - return {key}; - }, - }); - } + const actions = []; - const actions = []; + actions.push({ + render: annotation => { + const editAnnotationsTooltipText = ( + + ); + const editAnnotationsTooltipAriaLabelText = ( + + ); + return ( + + annotation$.next(annotation)} + iconType="pencil" + aria-label={editAnnotationsTooltipAriaLabelText} + /> + + ); + }, + }); + if (isSingleMetricViewerLinkVisible) { actions.push({ render: annotation => { - const editAnnotationsTooltipText = ( + const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id)); + const openInSingleMetricViewerTooltipText = isDrillDownAvailable ? ( + + ) : ( ); - const editAnnotationsTooltipAriaLabelText = ( + const openInSingleMetricViewerAriaLabelText = isDrillDownAvailable ? ( + ) : ( + ); + return ( - + annotation$.next(annotation)} - iconType="pencil" - aria-label={editAnnotationsTooltipAriaLabelText} + onClick={() => this.openSingleMetricView(annotation)} + disabled={!isDrillDownAvailable} + iconType="stats" + aria-label={openInSingleMetricViewerAriaLabelText} /> ); }, }); + } - if (isSingleMetricViewerLinkVisible) { - actions.push({ - render: annotation => { - const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id)); - const openInSingleMetricViewerTooltipText = isDrillDownAvailable ? ( - - ) : ( - - ); - const openInSingleMetricViewerAriaLabelText = isDrillDownAvailable ? ( - - ) : ( - - ); - - return ( - - this.openSingleMetricView(annotation)} - disabled={!isDrillDownAvailable} - iconType="stats" - aria-label={openInSingleMetricViewerAriaLabelText} - /> - - ); - }, - }); - } - - columns.push({ - align: RIGHT_ALIGNMENT, - width: '60px', - name: intl.formatMessage({ - id: 'xpack.ml.annotationsTable.actionsColumnName', - defaultMessage: 'Actions', - }), - actions, - }); - - const getRowProps = item => { - return { - onMouseOver: () => this.onMouseOverRow(item), - onMouseLeave: () => this.onMouseLeaveRow(), - }; + columns.push({ + align: RIGHT_ALIGNMENT, + width: '60px', + name: i18n.translate('xpack.ml.annotationsTable.actionsColumnName', { + defaultMessage: 'Actions', + }), + actions, + }); + + const getRowProps = item => { + return { + onMouseOver: () => this.onMouseOverRow(item), + onMouseLeave: () => this.onMouseLeaveRow(), }; + }; - return ( - - - - ); - } + return ( + + + + ); } -); - -export { AnnotationsTable }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js index 1d1b785600f97..11e196b1c8e3f 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js @@ -6,18 +6,12 @@ import jobConfig from '../../../../../common/types/__mocks__/job_config_farequote'; import mockAnnotations from './__mocks__/mock_annotations.json'; -import './annotations_table.test.mocks'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { AnnotationsTable } from './annotations_table'; -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - addBasePath: () => {}, -})); - jest.mock('../../../services/job_service', () => ({ mlJobService: { getJob: jest.fn(), @@ -38,19 +32,17 @@ jest.mock('../../../services/ml_api_service', () => { describe('AnnotationsTable', () => { test('Minimal initialization without props.', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('Initialization with job config prop.', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('Initialization with annotations prop.', () => { - const wrapper = shallowWithIntl( - - ); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts deleted file mode 100644 index 4a29fec03da85..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { chromeServiceMock } from '../../../../../../../../../src/core/public/mocks'; - -jest.doMock('ui/new_platform', () => ({ - npStart: { - core: { - chrome: chromeServiceMock.createStartContract(), - }, - }, -})); diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js index 074a584f3a136..c16dc37097b13 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -11,10 +11,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { toastNotifications } from 'ui/notify'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ES_FIELD_TYPES } from '../../../../../../../../src/plugins/data/public'; import { checkPermission } from '../../privilege/check_privilege'; @@ -29,465 +29,452 @@ 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'; - /* * Component for rendering the links menu inside a cell in the anomalies table. */ -export const LinksMenu = injectI18n( - class LinksMenu extends Component { - static propTypes = { - anomaly: PropTypes.object.isRequired, - bounds: PropTypes.object.isRequired, - showViewSeriesLink: PropTypes.bool, - isAggregatedData: PropTypes.bool, - interval: PropTypes.string, - showRuleEditorFlyout: PropTypes.func, +class LinksMenuUI extends Component { + static propTypes = { + anomaly: PropTypes.object.isRequired, + bounds: PropTypes.object.isRequired, + showViewSeriesLink: PropTypes.bool, + isAggregatedData: PropTypes.bool, + interval: PropTypes.string, + showRuleEditorFlyout: PropTypes.func, + }; + + constructor(props) { + super(props); + + this.state = { + isPopoverOpen: false, + toasts: [], }; + } - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - toasts: [], - }; - } - - openCustomUrl = customUrl => { - const { anomaly, interval, isAggregatedData, intl } = this.props; - - console.log('Anomalies Table - open customUrl for record:', anomaly); - - // If url_value contains $earliest$ and $latest$ tokens, add in times to the source record. - // Create a copy of the record as we are adding properties into it. - const record = _.cloneDeep(anomaly.source); - const timestamp = record.timestamp; - const configuredUrlValue = customUrl.url_value; - const timeRangeInterval = parseInterval(customUrl.time_range); - if (configuredUrlValue.includes('$earliest$')) { - let earliestMoment = moment(timestamp); - if (timeRangeInterval !== null) { - earliestMoment.subtract(timeRangeInterval); - } else { - earliestMoment = moment(timestamp).startOf(interval); - if (interval === 'hour') { - // Start from the previous hour. - earliestMoment.subtract(1, 'h'); - } + openCustomUrl = customUrl => { + const { anomaly, interval, isAggregatedData } = this.props; + + console.log('Anomalies Table - open customUrl for record:', anomaly); + + // If url_value contains $earliest$ and $latest$ tokens, add in times to the source record. + // Create a copy of the record as we are adding properties into it. + const record = _.cloneDeep(anomaly.source); + const timestamp = record.timestamp; + const configuredUrlValue = customUrl.url_value; + const timeRangeInterval = parseInterval(customUrl.time_range); + if (configuredUrlValue.includes('$earliest$')) { + let earliestMoment = moment(timestamp); + if (timeRangeInterval !== null) { + earliestMoment.subtract(timeRangeInterval); + } else { + earliestMoment = moment(timestamp).startOf(interval); + if (interval === 'hour') { + // Start from the previous hour. + earliestMoment.subtract(1, 'h'); } - record.earliest = earliestMoment.toISOString(); // e.g. 2016-02-08T16:00:00.000Z } + record.earliest = earliestMoment.toISOString(); // e.g. 2016-02-08T16:00:00.000Z + } - if (configuredUrlValue.includes('$latest$')) { - let latestMoment = moment(timestamp).add(record.bucket_span, 's'); - if (timeRangeInterval !== null) { - latestMoment.add(timeRangeInterval); - } else { - if (isAggregatedData === true) { - latestMoment = moment(timestamp).endOf(interval); - if (interval === 'hour') { - // Show to the end of the next hour. - latestMoment.add(1, 'h'); // e.g. 2016-02-08T18:59:59.999Z - } + if (configuredUrlValue.includes('$latest$')) { + let latestMoment = moment(timestamp).add(record.bucket_span, 's'); + if (timeRangeInterval !== null) { + latestMoment.add(timeRangeInterval); + } else { + if (isAggregatedData === true) { + latestMoment = moment(timestamp).endOf(interval); + if (interval === 'hour') { + // Show to the end of the next hour. + latestMoment.add(1, 'h'); // e.g. 2016-02-08T18:59:59.999Z } } - record.latest = latestMoment.toISOString(); } + record.latest = latestMoment.toISOString(); + } - // If url_value contains $mlcategoryterms$ or $mlcategoryregex$, add in the - // terms and regex for the selected categoryId to the source record. - if ( - (configuredUrlValue.includes('$mlcategoryterms$') || - configuredUrlValue.includes('$mlcategoryregex$')) && - _.has(record, 'mlcategory') - ) { - const jobId = record.job_id; - - // mlcategory in the source record will be an array - // - use first value (will only ever be more than one if influenced by category other than by/partition/over). - const categoryId = record.mlcategory[0]; - - ml.results - .getCategoryDefinition(jobId, categoryId) - .then(resp => { - // Prefix each of the terms with '+' so that the Elasticsearch Query String query - // run in a drilldown Kibana dashboard has to match on all terms. - const termsArray = resp.terms.split(' ').map(term => `+${term}`); - record.mlcategoryterms = termsArray.join(' '); - record.mlcategoryregex = resp.regex; - - // Replace any tokens in the configured url_value with values from the source record, - // and then open link in a new tab/window. - const urlPath = replaceStringTokens(customUrl.url_value, record, true); - openCustomUrlWindow(urlPath, customUrl); - }) - .catch(resp => { - console.log('openCustomUrl(): error loading categoryDefinition:', resp); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.anomaliesTable.linksMenu.unableToOpenLinkErrorMessage', - defaultMessage: - 'Unable to open link as an error occurred loading details on category ID {categoryId}', - }, - { - categoryId, - } - ) - ); - }); - } else { - // Replace any tokens in the configured url_value with values from the source record, - // and then open link in a new tab/window. - const urlPath = getUrlForRecord(customUrl, record); - openCustomUrlWindow(urlPath, customUrl); - } - }; + // If url_value contains $mlcategoryterms$ or $mlcategoryregex$, add in the + // terms and regex for the selected categoryId to the source record. + if ( + (configuredUrlValue.includes('$mlcategoryterms$') || + configuredUrlValue.includes('$mlcategoryregex$')) && + _.has(record, 'mlcategory') + ) { + const jobId = record.job_id; + + // mlcategory in the source record will be an array + // - use first value (will only ever be more than one if influenced by category other than by/partition/over). + const categoryId = record.mlcategory[0]; + + ml.results + .getCategoryDefinition(jobId, categoryId) + .then(resp => { + // Prefix each of the terms with '+' so that the Elasticsearch Query String query + // run in a drilldown Kibana dashboard has to match on all terms. + const termsArray = resp.terms.split(' ').map(term => `+${term}`); + record.mlcategoryterms = termsArray.join(' '); + record.mlcategoryregex = resp.regex; + + // Replace any tokens in the configured url_value with values from the source record, + // and then open link in a new tab/window. + const urlPath = replaceStringTokens(customUrl.url_value, record, true); + openCustomUrlWindow(urlPath, customUrl); + }) + .catch(resp => { + console.log('openCustomUrl(): error loading categoryDefinition:', resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.anomaliesTable.linksMenu.unableToOpenLinkErrorMessage', { + defaultMessage: + 'Unable to open link as an error occurred loading details on category ID {categoryId}', + values: { + categoryId, + }, + }) + ); + }); + } else { + // Replace any tokens in the configured url_value with values from the source record, + // and then open link in a new tab/window. + const urlPath = getUrlForRecord(customUrl, record); + openCustomUrlWindow(urlPath, customUrl); + } + }; - viewSeries = () => { - const record = this.props.anomaly.source; - const bounds = this.props.bounds; - const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z - const to = bounds.max.toISOString(); + viewSeries = () => { + const record = this.props.anomaly.source; + const bounds = this.props.bounds; + const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z + const to = bounds.max.toISOString(); - // Zoom to show 50 buckets either side of the record. - const recordTime = moment(record.timestamp); - const zoomFrom = recordTime.subtract(50 * record.bucket_span, 's').toISOString(); - const zoomTo = recordTime.add(100 * record.bucket_span, 's').toISOString(); + // Zoom to show 50 buckets either side of the record. + const recordTime = moment(record.timestamp); + const zoomFrom = recordTime.subtract(50 * record.bucket_span, 's').toISOString(); + const zoomTo = recordTime.add(100 * record.bucket_span, 's').toISOString(); - // Extract the by, over and partition fields for the record. - const entityCondition = {}; + // Extract the by, over and partition fields for the record. + const entityCondition = {}; - if (_.has(record, 'partition_field_value')) { - entityCondition[record.partition_field_name] = record.partition_field_value; - } + if (_.has(record, 'partition_field_value')) { + entityCondition[record.partition_field_name] = record.partition_field_value; + } - if (_.has(record, 'over_field_value')) { - entityCondition[record.over_field_name] = record.over_field_value; - } + if (_.has(record, 'over_field_value')) { + entityCondition[record.over_field_name] = record.over_field_value; + } - if (_.has(record, 'by_field_value')) { - // Note that analyses with by and over fields, will have a top-level by_field_name, - // but the by_field_value(s) will be in the nested causes array. - // TODO - drilldown from cause in expanded row only? - entityCondition[record.by_field_name] = record.by_field_value; - } + if (_.has(record, 'by_field_value')) { + // Note that analyses with by and over fields, will have a top-level by_field_name, + // but the by_field_value(s) will be in the nested causes array. + // TODO - drilldown from cause in expanded row only? + entityCondition[record.by_field_name] = record.by_field_value; + } - // Use rison to build the URL . - const _g = rison.encode({ - ml: { - jobIds: [record.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, + // Use rison to build the URL . + const _g = rison.encode({ + ml: { + jobIds: [record.job_id], + }, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + time: { + from: from, + to: to, + mode: 'absolute', + }, + }); + + const _a = rison.encode({ + mlTimeSeriesExplorer: { + zoom: { + from: zoomFrom, + to: zoomTo, }, - time: { - from: from, - to: to, - mode: 'absolute', + detectorIndex: record.detector_index, + entities: entityCondition, + }, + query: { + query_string: { + analyze_wildcard: true, + query: '*', }, - }); - - const _a = rison.encode({ - mlTimeSeriesExplorer: { - 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'); + }; + + viewExamples = () => { + const categoryId = this.props.anomaly.entityValue; + const record = this.props.anomaly.source; + + const job = mlJobService.getJob(this.props.anomaly.jobId); + if (job === undefined) { + console.log(`viewExamples(): no job found with ID: ${this.props.anomaly.jobId}`); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.anomaliesTable.linksMenu.unableToViewExamplesErrorMessage', { + defaultMessage: 'Unable to view examples as no details could be found for job ID {jobId}', + values: { + jobId: this.props.anomaly.jobId, }, - }, - }); - - // 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'); - }; - - viewExamples = () => { - const { intl } = this.props; - const categoryId = this.props.anomaly.entityValue; - const record = this.props.anomaly.source; - - const job = mlJobService.getJob(this.props.anomaly.jobId); - if (job === undefined) { - console.log(`viewExamples(): no job found with ID: ${this.props.anomaly.jobId}`); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.anomaliesTable.linksMenu.unableToViewExamplesErrorMessage', - defaultMessage: - 'Unable to view examples as no details could be found for job ID {jobId}', - }, - { - jobId: this.props.anomaly.jobId, - } - ) - ); - return; - } - const categorizationFieldName = job.analysis_config.categorization_field_name; - const datafeedIndices = job.datafeed_config.indices; - // Find the type of the categorization field i.e. text (preferred) or keyword. - // Uses the first matching field found in the list of indices in the datafeed_config. - // attempt to load the field type using each index. we have to do it this way as _field_caps - // doesn't specify which index a field came from unless there is a clash. - let i = 0; - findFieldType(datafeedIndices[i]); - - function findFieldType(index) { - getFieldTypeFromMapping(index, categorizationFieldName) - .then(resp => { - if (resp !== '') { - createAndOpenUrl(index, resp); + }) + ); + return; + } + const categorizationFieldName = job.analysis_config.categorization_field_name; + const datafeedIndices = job.datafeed_config.indices; + // Find the type of the categorization field i.e. text (preferred) or keyword. + // Uses the first matching field found in the list of indices in the datafeed_config. + // attempt to load the field type using each index. we have to do it this way as _field_caps + // doesn't specify which index a field came from unless there is a clash. + let i = 0; + findFieldType(datafeedIndices[i]); + + function findFieldType(index) { + getFieldTypeFromMapping(index, categorizationFieldName) + .then(resp => { + if (resp !== '') { + createAndOpenUrl(index, resp); + } else { + i++; + if (i < datafeedIndices.length) { + findFieldType(datafeedIndices[i]); } else { - i++; - if (i < datafeedIndices.length) { - findFieldType(datafeedIndices[i]); - } else { - error(); - } + error(); } - }) - .catch(() => { - error(); - }); - } + } + }) + .catch(() => { + error(); + }); + } - function createAndOpenUrl(index, categorizationFieldType) { - // Find the ID of the index pattern with a title attribute which matches the - // index configured in the datafeed. If a Kibana index pattern has not been created - // for this index, then the user will see a warning message on the Discover tab advising - // them that no matching index pattern has been configured. - const indexPatternId = getIndexPatternIdFromName(index) || index; - - // Get the definition of the category and use the terms or regex to view the - // matching events in the Kibana Discover tab depending on whether the - // categorization field is of mapping type text (preferred) or keyword. - ml.results - .getCategoryDefinition(record.job_id, categoryId) - .then(resp => { - let query = null; - // Build query using categorization regex (if keyword type) or terms (if text type). - // Check for terms or regex in case categoryId represents an anomaly from the absence of the - // categorization field in documents (usually indicated by a categoryId of -1). - if (categorizationFieldType === ES_FIELD_TYPES.KEYWORD) { - if (resp.regex) { - query = { - language: SEARCH_QUERY_LANGUAGE.LUCENE, - query: `${categorizationFieldName}:/${resp.regex}/`, - }; - } - } else { - if (resp.terms) { - const escapedTerms = escapeDoubleQuotes(resp.terms); - query = { - language: SEARCH_QUERY_LANGUAGE.KUERY, - query: - `${categorizationFieldName}:"` + - escapedTerms.split(' ').join(`" and ${categorizationFieldName}:"`) + - '"', - }; - } + function createAndOpenUrl(index, categorizationFieldType) { + // Find the ID of the index pattern with a title attribute which matches the + // index configured in the datafeed. If a Kibana index pattern has not been created + // for this index, then the user will see a warning message on the Discover tab advising + // them that no matching index pattern has been configured. + const indexPatternId = getIndexPatternIdFromName(index) || index; + + // Get the definition of the category and use the terms or regex to view the + // matching events in the Kibana Discover tab depending on whether the + // categorization field is of mapping type text (preferred) or keyword. + ml.results + .getCategoryDefinition(record.job_id, categoryId) + .then(resp => { + let query = null; + // Build query using categorization regex (if keyword type) or terms (if text type). + // Check for terms or regex in case categoryId represents an anomaly from the absence of the + // categorization field in documents (usually indicated by a categoryId of -1). + if (categorizationFieldType === ES_FIELD_TYPES.KEYWORD) { + if (resp.regex) { + query = { + language: SEARCH_QUERY_LANGUAGE.LUCENE, + query: `${categorizationFieldName}:/${resp.regex}/`, + }; } - - const recordTime = moment(record.timestamp); - const from = recordTime.toISOString(); - const to = recordTime.add(record.bucket_span, 's').toISOString(); - - // Use rison to build the URL . - const _g = rison.encode({ - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from: from, - to: to, - mode: 'absolute', - }, - }); - - const appStateProps = { - index: indexPatternId, - filters: [], - }; - if (query !== null) { - appStateProps.query = query; + } else { + if (resp.terms) { + const escapedTerms = escapeDoubleQuotes(resp.terms); + query = { + language: SEARCH_QUERY_LANGUAGE.KUERY, + query: + `${categorizationFieldName}:"` + + escapedTerms.split(' ').join(`" and ${categorizationFieldName}:"`) + + '"', + }; } - const _a = rison.encode(appStateProps); - - // Need to encode the _a parameter as it will contain characters such as '+' if using the regex. - let path = chrome.getBasePath(); - path += '/app/kibana#/discover'; - path += '?_g=' + _g; - path += '&_a=' + encodeURIComponent(_a); - window.open(path, '_blank'); - }) - .catch(resp => { - console.log('viewExamples(): error loading categoryDefinition:', resp); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.anomaliesTable.linksMenu.loadingDetailsErrorMessage', - defaultMessage: - 'Unable to view examples as an error occurred loading details on category ID {categoryId}', - }, - { - categoryId, - } - ) - ); - }); - } - - function error() { - console.log( - `viewExamples(): error finding type of field ${categorizationFieldName} in indices:`, - datafeedIndices - ); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', - defaultMessage: - 'Unable to view examples of documents with mlcategory {categoryId} ' + - 'as no mapping could be found for the categorization field {categorizationFieldName}', - }, - { - categoryId, - categorizationFieldName, - } - ) - ); - } - }; + } - onButtonClick = () => { - this.setState(prevState => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); - }; + const recordTime = moment(record.timestamp); + const from = recordTime.toISOString(); + const to = recordTime.add(record.bucket_span, 's').toISOString(); - closePopover = () => { - this.setState({ - isPopoverOpen: false, - }); - }; - - render() { - const { anomaly, showViewSeriesLink, intl } = this.props; - const canConfigureRules = isRuleSupported(anomaly.source) && checkPermission('canUpdateJob'); - - const button = ( - - ); + time: { + from: from, + to: to, + mode: 'absolute', + }, + }); - const items = []; - if (anomaly.customUrls !== undefined) { - anomaly.customUrls.forEach((customUrl, index) => { - items.push( - { - this.closePopover(); - this.openCustomUrl(customUrl); - }} - > - {customUrl.url_name} - + const appStateProps = { + index: indexPatternId, + filters: [], + }; + if (query !== null) { + appStateProps.query = query; + } + const _a = rison.encode(appStateProps); + + // Need to encode the _a parameter as it will contain characters such as '+' if using the regex. + const { basePath } = this.props.kibana.services.http; + let path = basePath.get(); + path += '/app/kibana#/discover'; + path += '?_g=' + _g; + path += '&_a=' + encodeURIComponent(_a); + window.open(path, '_blank'); + }) + .catch(resp => { + console.log('viewExamples(): error loading categoryDefinition:', resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.anomaliesTable.linksMenu.loadingDetailsErrorMessage', { + defaultMessage: + 'Unable to view examples as an error occurred loading details on category ID {categoryId}', + values: { + categoryId, + }, + }) ); }); - } - - if (showViewSeriesLink === true && anomaly.isTimeSeriesViewRecord === true) { - items.push( - { - this.closePopover(); - this.viewSeries(); - }} - > - - - ); - } + } - if (anomaly.entityName === 'mlcategory') { + function error() { + console.log( + `viewExamples(): error finding type of field ${categorizationFieldName} in indices:`, + datafeedIndices + ); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', { + defaultMessage: + 'Unable to view examples of documents with mlcategory {categoryId} ' + + 'as no mapping could be found for the categorization field {categorizationFieldName}', + values: { + categoryId, + categorizationFieldName, + }, + }) + ); + } + }; + + onButtonClick = () => { + this.setState(prevState => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + render() { + const { anomaly, showViewSeriesLink } = this.props; + const canConfigureRules = isRuleSupported(anomaly.source) && checkPermission('canUpdateJob'); + + const button = ( + + ); + + const items = []; + if (anomaly.customUrls !== undefined) { + anomaly.customUrls.forEach((customUrl, index) => { items.push( { this.closePopover(); - this.viewExamples(); + this.openCustomUrl(customUrl); }} > - + {customUrl.url_name} ); - } + }); + } - if (canConfigureRules) { - items.push( - { - this.closePopover(); - this.props.showRuleEditorFlyout(anomaly); - }} - > - - - ); - } + if (showViewSeriesLink === true && anomaly.isTimeSeriesViewRecord === true) { + items.push( + { + this.closePopover(); + this.viewSeries(); + }} + > + + + ); + } - return ( - { + this.closePopover(); + this.viewExamples(); + }} > - - + + ); } + + if (canConfigureRules) { + items.push( + { + this.closePopover(); + this.props.showRuleEditorFlyout(anomaly); + }} + > + + + ); + } + + return ( + + + + ); } -); +} + +export const LinksMenu = withKibana(LinksMenuUI); diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts index f047ae800266b..7b113326a1f97 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts @@ -6,8 +6,6 @@ import { influencerColorScaleFactory } from './use_color_range'; -jest.mock('../../contexts/ui/use_ui_chrome_context'); - describe('useColorRange', () => { test('influencerColorScaleFactory(1)', () => { const influencerColorScale = influencerColorScaleFactory(1); diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts index f9c5e6ff81f9e..83f143b75b388 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -11,7 +11,7 @@ import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; import { i18n } from '@kbn/i18n'; -import { useUiChromeContext } from '../../contexts/ui/use_ui_chrome_context'; +import { useUiSettings } from '../../contexts/kibana/use_ui_settings_context'; /** * Custom color scale factory that takes the amount of feature influencers @@ -150,11 +150,7 @@ export const useColorRange = ( colorRangeScale = COLOR_RANGE_SCALE.LINEAR, featureCount = 1 ) => { - const euiTheme = useUiChromeContext() - .getUiSettingsClient() - .get('theme:darkMode') - ? euiThemeDark - : euiThemeLight; + const euiTheme = useUiSettings().get('theme:darkMode') ? euiThemeDark : euiThemeLight; const colorRanges = { [COLOR_RANGE.BLUE]: [d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorVis1)], diff --git a/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js index 1b06b72d1387c..056fd04857cba 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js @@ -7,10 +7,6 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; -jest.mock('ui/i18n', () => ({ - I18nContext: jest.fn(), -})); - import { FieldTitleBar } from './field_title_bar'; // helper to let PropTypes throw errors instead of just doing console.error() diff --git a/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx index 4460ced7079c3..d0fde87bf1c2a 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx @@ -7,10 +7,9 @@ import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Query } from 'src/plugins/data/public'; +import { Query, IndexPattern } from 'src/plugins/data/public'; import { EuiButton } from '@elastic/eui'; import { setFullTimeRange } from './full_time_range_selector_service'; -import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; interface Props { indexPattern: IndexPattern; diff --git a/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts index e69aaf2ede037..265e11ce6a154 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts @@ -7,10 +7,9 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; -import { timefilter } from 'ui/timefilter'; import { Query } from 'src/plugins/data/public'; import dateMath from '@elastic/datemath'; +import { getTimefilter, getToastNotifications } from '../../util/dependency_cache'; import { ml, GetTimeFieldRangeResponse } from '../../services/ml_api_service'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; @@ -24,6 +23,7 @@ export async function setFullTimeRange( query: Query ): Promise { try { + const timefilter = getTimefilter(); const resp = await ml.getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, @@ -35,6 +35,7 @@ export async function setFullTimeRange( }); return resp; } catch (resp) { + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.fullTimeRangeSelector.errorSettingTimeRangeNotification', { defaultMessage: 'An error occurred setting the time range.', @@ -45,20 +46,12 @@ export async function setFullTimeRange( } export function getTimeFilterRange(): TimeRange { - let from = 0; - let to = 0; - const fromString = timefilter.getTime().from; - const toString = timefilter.getTime().to; - if (typeof fromString === 'string' && typeof toString === 'string') { - const fromMoment = dateMath.parse(fromString); - const toMoment = dateMath.parse(toString); - if (typeof fromMoment !== 'undefined' && typeof toMoment !== 'undefined') { - const fromMs = fromMoment.valueOf(); - const toMs = toMoment.valueOf(); - from = fromMs; - to = toMs; - } - } + const timefilter = getTimefilter(); + const fromMoment = dateMath.parse(timefilter.getTime().from); + const toMoment = dateMath.parse(timefilter.getTime().to); + const from = fromMoment !== undefined ? fromMoment.valueOf() : 0; + const to = toMoment !== undefined ? toMoment.valueOf() : 0; + return { to, from, diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx index f1d9dcb0ec795..bd2ec2d1511a3 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -22,8 +22,7 @@ import { import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; - +import { useMlKibana } from '../../contexts/kibana'; import { Dictionary } from '../../../../common/types/common'; import { MlJobWithTimeRange } from '../../../../common/types/jobs'; import { ml } from '../../services/ml_api_service'; @@ -114,6 +113,9 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [ganttBarWidth, setGanttBarWidth] = useState(DEFAULT_GANTT_BAR_WIDTH); const flyoutEl = useRef<{ flyout: HTMLElement }>(null); + const { + services: { notifications }, + } = useMlKibana(); // Ensure JobSelectionBar gets updated when selection via globalState changes. useEffect(() => { @@ -178,7 +180,8 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J }) .catch((err: any) => { console.error('Error fetching jobs with time range', err); // eslint-disable-line - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.jobSelector.jobFetchErrorMessage', { defaultMessage: 'An error occurred fetching jobs. Refresh and try again.', }), diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts b/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts index 563156ea98055..214bb90917302 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts @@ -7,9 +7,9 @@ import { difference } from 'lodash'; import { useEffect } from 'react'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; +import { getToastNotifications } from '../../util/dependency_cache'; import { MlJobWithTimeRange } from '../../../../common/types/jobs'; import { useUrlState } from '../../util/url_state'; @@ -27,6 +27,7 @@ function getInvalidJobIds(jobs: MlJobWithTimeRange[], ids: string[]) { function warnAboutInvalidJobIds(invalidIds: string[]) { if (invalidIds.length > 0) { + const toastNotifications = getToastNotifications(); toastNotifications.addWarning( i18n.translate('xpack.ml.jobSelect.requestedJobsDoesNotExistWarningMessage', { defaultMessage: `Requested @@ -66,6 +67,7 @@ export const useJobSelection = (jobs: MlJobWithTimeRange[], dateFormatTz: string useEffect(() => { // if there are no valid ids, warn and then select the first job if (validIds.length === 0 && jobs.length > 0) { + const toastNotifications = getToastNotifications(); toastNotifications.addWarning( i18n.translate('xpack.ml.jobSelect.noJobsSelectedWarningMessage', { defaultMessage: 'No jobs selected, auto selecting first job', diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js index e604c101a9994..0f3c6d25fe641 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js @@ -10,15 +10,16 @@ import { uniqueId } from 'lodash'; import { FilterBar } from './filter_bar'; import { EuiCallOut, EuiLink, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { metadata } from 'ui/metadata'; import { getSuggestions, getKqlQueryValues } from './utils'; +import { getDocLinks } from '../../util/dependency_cache'; function getErrorWithLink(errorMessage) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = getDocLinks(); return ( {`${errorMessage} Input must be valid `} {'Kibana Query Language'} diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js index 4e74a4bd545a3..610d924651406 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js @@ -8,8 +8,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import { KqlFilterBar } from './kql_filter_bar'; -jest.mock('ui/new_platform'); - const defaultProps = { indexPattern: { title: '.ml-anomalies-*', @@ -33,6 +31,12 @@ const defaultProps = { placeholder: undefined, }; +jest.mock('../../util/dependency_cache', () => ({ + getAutocomplete: () => ({ + getQuerySuggestions: () => {}, + }), +})); + describe('KqlFilterBar', () => { test('snapshot', () => { const wrapper = shallow(); diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js index bb7b143c948d8..bb3e676f4b410 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { npStart } from 'ui/new_platform'; import { esKuery } from '../../../../../../../../src/plugins/data/public'; +import { getAutocomplete } from '../../util/dependency_cache'; export function getSuggestions(query, selectionStart, indexPattern, boolFilter) { - return npStart.plugins.data.autocomplete.getQuerySuggestions({ + const autocomplete = getAutocomplete(); + return autocomplete.getQuerySuggestions({ language: 'kuery', indexPatterns: [indexPattern], boolFilter, diff --git a/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js b/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js index 6d5f4e267abcf..d79fe14cbac4e 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js +++ b/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../util/dependency_cache'; import { MLRequestFailure } from '../../util/ml_error'; import { i18n } from '@kbn/i18n'; @@ -18,6 +18,7 @@ function errorNotify(text, resp) { err = new Error(text); } + const toastNotifications = getToastNotifications(); toastNotifications.addError(new MLRequestFailure(err, resp), { title: i18n.translate('xpack.ml.messagebarService.errorTitle', { defaultMessage: 'An error has ocurred', diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx index e9bec02868b71..b03281bf30399 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx @@ -10,15 +10,36 @@ import { MemoryRouter } from 'react-router-dom'; import { EuiSuperDatePicker } from '@elastic/eui'; -import { uiTimefilterMock } from '../../../contexts/ui/__mocks__/mocks_jest'; import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; import { TopNav } from './top_nav'; -uiTimefilterMock.enableAutoRefreshSelector(); -uiTimefilterMock.enableTimeRangeSelector(); - -jest.mock('../../../contexts/ui/use_ui_context'); +jest.mock('../../../contexts/kibana', () => ({ + useMlKibana: () => { + return { + services: { + uiSettings: { get: jest.fn() }, + data: { + query: { + timefilter: { + timefilter: { + getRefreshInterval: jest.fn(), + setRefreshInterval: jest.fn(), + getTime: jest.fn(), + isAutoRefreshSelectorEnabled: jest.fn(), + isTimeRangeSelectorEnabled: jest.fn(), + getRefreshIntervalUpdate$: jest.fn(), + getTimeUpdate$: jest.fn(), + getEnabledUpdated$: jest.fn(), + }, + history: { get: jest.fn() }, + }, + }, + }, + }, + }; + }, +})); const noop = () => {}; @@ -41,7 +62,6 @@ describe('Navigation Menu: ', () => { ); expect(wrapper.find(TopNav)).toHaveLength(1); - expect(wrapper.find('EuiSuperDatePicker')).toHaveLength(1); expect(refreshListener).toBeCalledTimes(0); refreshSubscription.unsubscribe(); diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx index a63a07b3ec538..edc6aece265f3 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx @@ -7,15 +7,14 @@ import React, { FC, Fragment, useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; import { EuiSuperDatePicker, OnRefreshProps } from '@elastic/eui'; -import { TimeHistory } from 'ui/timefilter'; -import { TimeRange } from 'src/plugins/data/public'; +import { TimeRange, TimeHistoryContract } from 'src/plugins/data/public'; import { mlTimefilterRefresh$, mlTimefilterTimeChange$, } from '../../../services/timefilter_refresh_service'; -import { useUiContext } from '../../../contexts/ui/use_ui_context'; import { useUrlState } from '../../../util/url_state'; +import { useMlKibana } from '../../../contexts/kibana'; interface Duration { start: string; @@ -27,7 +26,7 @@ interface RefreshInterval { value: number; } -function getRecentlyUsedRangesFactory(timeHistory: TimeHistory) { +function getRecentlyUsedRangesFactory(timeHistory: TimeHistoryContract) { return function(): Duration[] { return ( timeHistory.get()?.map(({ from, to }: TimeRange) => { @@ -45,9 +44,12 @@ function updateLastRefresh(timeRange: OnRefreshProps) { } export const TopNav: FC = () => { - const { chrome, timefilter, timeHistory } = useUiContext(); + const { services } = useMlKibana(); + const config = services.uiSettings; + const { timefilter, history } = services.data.query.timefilter; + const [globalState, setGlobalState] = useUrlState('_g'); - const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(timeHistory); + const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(history); const [refreshInterval, setRefreshInterval] = useState( globalState?.refreshInterval ?? timefilter.getRefreshInterval() @@ -66,7 +68,7 @@ export const TopNav: FC = () => { timefilter.isTimeRangeSelectorEnabled() ); - const dateFormat = chrome.getUiSettingsClient().get('dateFormat'); + const dateFormat = config.get('dateFormat'); useEffect(() => { const subscriptions = new Subscription(); diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap index da9a3c7437bf4..5d8c644d6d0eb 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap @@ -40,7 +40,7 @@ exports[`ConditionsSectionExpression renders when enabled with no conditions sup exports[`ConditionsSectionExpression renders when enabled with one condition 1`] = ` - - - { - this.setState({ - isAppliesToOpen: true, - isOperatorValueOpen: false, - }); - }; - - closeAppliesTo = () => { - this.setState({ - isAppliesToOpen: false, - }); - }; - - openOperatorValue = () => { - this.setState({ - isAppliesToOpen: false, - isOperatorValueOpen: true, - }); - }; - - closeOperatorValue = () => { - this.setState({ - isOperatorValueOpen: false, - }); - }; - - changeAppliesTo = event => { - const { index, operator, value, updateCondition } = this.props; - updateCondition(index, event.target.value, operator, value); - }; - - changeOperator = event => { - const { index, appliesTo, value, updateCondition } = this.props; - updateCondition(index, appliesTo, event.target.value, value); - }; - - changeValue = event => { - const { index, appliesTo, operator, updateCondition } = this.props; - updateCondition(index, appliesTo, operator, +event.target.value); +export class ConditionExpression extends Component { + static propTypes = { + index: PropTypes.number.isRequired, + appliesTo: PropTypes.oneOf([ + APPLIES_TO.ACTUAL, + APPLIES_TO.TYPICAL, + APPLIES_TO.DIFF_FROM_TYPICAL, + ]), + operator: PropTypes.oneOf([ + OPERATOR.LESS_THAN, + OPERATOR.LESS_THAN_OR_EQUAL, + OPERATOR.GREATER_THAN, + OPERATOR.GREATER_THAN_OR_EQUAL, + ]), + value: PropTypes.number.isRequired, + updateCondition: PropTypes.func.isRequired, + deleteCondition: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + isAppliesToOpen: false, + isOperatorValueOpen: false, }; + } - renderAppliesToPopover() { - return ( -
- - - -
- -
+ openAppliesTo = () => { + this.setState({ + isAppliesToOpen: true, + isOperatorValueOpen: false, + }); + }; + + closeAppliesTo = () => { + this.setState({ + isAppliesToOpen: false, + }); + }; + + openOperatorValue = () => { + this.setState({ + isAppliesToOpen: false, + isOperatorValueOpen: true, + }); + }; + + closeOperatorValue = () => { + this.setState({ + isOperatorValueOpen: false, + }); + }; + + changeAppliesTo = event => { + const { index, operator, value, updateCondition } = this.props; + updateCondition(index, event.target.value, operator, value); + }; + + changeOperator = event => { + const { index, appliesTo, value, updateCondition } = this.props; + updateCondition(index, appliesTo, event.target.value, value); + }; + + changeValue = event => { + const { index, appliesTo, operator, updateCondition } = this.props; + updateCondition(index, appliesTo, operator, +event.target.value); + }; + + renderAppliesToPopover() { + return ( +
+ + + +
+
- ); - } - - renderOperatorValuePopover() { - return ( -
- - - -
- - - - - - - - - -
+
+ ); + } + + renderOperatorValuePopover() { + return ( +
+ + + +
+ + + + + + + + +
- ); - } - - render() { - const { index, appliesTo, operator, value, deleteCondition } = this.props; - - return ( - - - - } - value={appliesToText(appliesTo)} - isActive={this.state.isAppliesToOpen} - onClick={this.openAppliesTo} - /> - } - isOpen={this.state.isAppliesToOpen} - closePopover={this.closeAppliesTo} - panelPaddingSize="none" - ownFocus - withTitle - anchorPosition="downLeft" - > - {this.renderAppliesToPopover()} - - - - - - } - value={`${value}`} - isActive={this.state.isOperatorValueOpen} - onClick={this.openOperatorValue} - /> - } - isOpen={this.state.isOperatorValueOpen} - closePopover={this.closeOperatorValue} - panelPaddingSize="none" - ownFocus - withTitle - anchorPosition="downLeft" - > - {this.renderOperatorValuePopover()} - - - - deleteCondition(index)} - iconType="trash" - aria-label={this.props.intl.formatMessage({ - id: 'xpack.ml.ruleEditor.conditionExpression.deleteConditionButtonAriaLabel', +
+ ); + } + + render() { + const { index, appliesTo, operator, value, deleteCondition } = this.props; + + return ( + + + + } + value={appliesToText(appliesTo)} + isActive={this.state.isAppliesToOpen} + onClick={this.openAppliesTo} + /> + } + isOpen={this.state.isAppliesToOpen} + closePopover={this.closeAppliesTo} + panelPaddingSize="none" + ownFocus + withTitle + anchorPosition="downLeft" + > + {this.renderAppliesToPopover()} + + + + + + } + value={`${value}`} + isActive={this.state.isOperatorValueOpen} + onClick={this.openOperatorValue} + /> + } + isOpen={this.state.isOperatorValueOpen} + closePopover={this.closeOperatorValue} + panelPaddingSize="none" + ownFocus + withTitle + anchorPosition="downLeft" + > + {this.renderOperatorValuePopover()} + + + + deleteCondition(index)} + iconType="trash" + aria-label={i18n.translate( + 'xpack.ml.ruleEditor.conditionExpression.deleteConditionButtonAriaLabel', + { defaultMessage: 'Delete condition', - })} - /> - - - ); - } + } + )} + /> + + + ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js index eaab9c2ad7a62..79ed620d151f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js @@ -29,7 +29,7 @@ describe('ConditionExpression', () => { value: 123, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); @@ -42,7 +42,7 @@ describe('ConditionExpression', () => { value: 123, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index 1f66cf95553b9..6dabf78b31002 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -28,8 +28,6 @@ import { EuiTitle, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; - import { DetectorDescriptionList } from './components/detector_description_list'; import { ActionsSection } from './actions_section'; import { checkPermission } from '../../privilege/check_privilege'; @@ -50,682 +48,679 @@ import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS, } from '../../../../common/constants/detector_rule'; import { getPartitioningFieldNames } from '../../../../common/util/job_utils'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; -import { metadata } from 'ui/metadata'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +class RuleEditorFlyoutUI extends Component { + static propTypes = { + setShowFunction: PropTypes.func.isRequired, + unsetShowFunction: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + anomaly: {}, + job: {}, + ruleIndex: -1, + rule: getNewRuleDefaults(), + skipModelUpdate: false, + isConditionsEnabled: false, + isScopeEnabled: false, + filterListIds: [], + isFlyoutVisible: false, + }; -// metadata.branch corresponds to the version used in documentation links. -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-rules.html`; + this.partitioningFieldNames = []; + this.canGetFilters = checkPermission('canGetFilters'); + } -export const RuleEditorFlyout = injectI18n( - class RuleEditorFlyout extends Component { - static propTypes = { - setShowFunction: PropTypes.func.isRequired, - unsetShowFunction: PropTypes.func.isRequired, - }; + componentDidMount() { + if (typeof this.props.setShowFunction === 'function') { + this.props.setShowFunction(this.showFlyout); + } + } - constructor(props) { - super(props); - - this.state = { - anomaly: {}, - job: {}, - ruleIndex: -1, - rule: getNewRuleDefaults(), - skipModelUpdate: false, - isConditionsEnabled: false, - isScopeEnabled: false, - filterListIds: [], + componentWillUnmount() { + if (typeof this.props.unsetShowFunction === 'function') { + this.props.unsetShowFunction(); + } + } + + showFlyout = anomaly => { + let ruleIndex = -1; + const job = mlJobService.getJob(anomaly.jobId); + if (job === undefined) { + // No details found for this job, display an error and + // don't open the Flyout as no edits can be made without the job. + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.unableToConfigureRulesNotificationMesssage', + { + defaultMessage: + 'Unable to configure rules as an error occurred obtaining details for job ID {jobId}', + values: { jobId: anomaly.jobId }, + } + ) + ); + this.setState({ + job, isFlyoutVisible: false, - }; + }); - this.partitioningFieldNames = []; - this.canGetFilters = checkPermission('canGetFilters'); + return; } - componentDidMount() { - if (typeof this.props.setShowFunction === 'function') { - this.props.setShowFunction(this.showFlyout); - } + this.partitioningFieldNames = getPartitioningFieldNames(job, anomaly.detectorIndex); + + // Check if any rules are configured for this detector. + const detectorIndex = anomaly.detectorIndex; + const detector = job.analysis_config.detectors[detectorIndex]; + if (detector.custom_rules === undefined) { + ruleIndex = 0; } - componentWillUnmount() { - if (typeof this.props.unsetShowFunction === 'function') { - this.props.unsetShowFunction(); - } + let isConditionsEnabled = false; + if (ruleIndex === 0) { + // Configuring the first rule for a detector. + isConditionsEnabled = this.partitioningFieldNames.length === 0; } - showFlyout = anomaly => { - let ruleIndex = -1; - const { intl } = this.props; - const job = mlJobService.getJob(anomaly.jobId); - if (job === undefined) { - // No details found for this job, display an error and - // don't open the Flyout as no edits can be made without the job. - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.ruleEditor.ruleEditorFlyout.unableToConfigureRulesNotificationMesssage', - defaultMessage: - 'Unable to configure rules as an error occurred obtaining details for job ID {jobId}', - }, - { jobId: anomaly.jobId } - ) - ); - this.setState({ - job, - isFlyoutVisible: false, + this.setState({ + anomaly, + job, + ruleIndex, + isConditionsEnabled, + isScopeEnabled: false, + isFlyoutVisible: true, + }); + + if (this.partitioningFieldNames.length > 0 && this.canGetFilters) { + // Load the current list of filters. These are used for configuring rule scope. + ml.filters + .filters() + .then(filters => { + const filterListIds = filters.map(filter => filter.filter_id); + this.setState({ + filterListIds, + }); + }) + .catch(resp => { + console.log('Error loading list of filters:', resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithLoadingFilterListsNotificationMesssage', + { + defaultMessage: 'Error loading the filter lists used in the rule scope', + } + ) + ); }); + } + }; + + closeFlyout = () => { + this.setState({ isFlyoutVisible: false }); + }; + + setEditRuleIndex = ruleIndex => { + const detectorIndex = this.state.anomaly.detectorIndex; + const detector = this.state.job.analysis_config.detectors[detectorIndex]; + const rules = detector.custom_rules; + const rule = + rules === undefined || ruleIndex >= rules.length ? getNewRuleDefaults() : rules[ruleIndex]; + + const isConditionsEnabled = + this.partitioningFieldNames.length === 0 || + (rule.conditions !== undefined && rule.conditions.length > 0); + const isScopeEnabled = rule.scope !== undefined && Object.keys(rule.scope).length > 0; + if (isScopeEnabled === true) { + // Add 'enabled:true' to mark them as selected in the UI. + Object.keys(rule.scope).forEach(field => { + rule.scope[field].enabled = true; + }); + } - return; + this.setState({ + ruleIndex, + rule, + isConditionsEnabled, + isScopeEnabled, + }); + }; + + onSkipResultChange = e => { + const checked = e.target.checked; + this.setState(prevState => { + const actions = [...prevState.rule.actions]; + const idx = actions.indexOf(ACTION.SKIP_RESULT); + if (idx === -1 && checked) { + actions.push(ACTION.SKIP_RESULT); + } else if (idx > -1 && !checked) { + actions.splice(idx, 1); } - this.partitioningFieldNames = getPartitioningFieldNames(job, anomaly.detectorIndex); - - // Check if any rules are configured for this detector. - const detectorIndex = anomaly.detectorIndex; - const detector = job.analysis_config.detectors[detectorIndex]; - if (detector.custom_rules === undefined) { - ruleIndex = 0; + return { + rule: { ...prevState.rule, actions }, + }; + }); + }; + + onSkipModelUpdateChange = e => { + const checked = e.target.checked; + this.setState(prevState => { + const actions = [...prevState.rule.actions]; + const idx = actions.indexOf(ACTION.SKIP_MODEL_UPDATE); + if (idx === -1 && checked) { + actions.push(ACTION.SKIP_MODEL_UPDATE); + } else if (idx > -1 && !checked) { + actions.splice(idx, 1); } - let isConditionsEnabled = false; - if (ruleIndex === 0) { - // Configuring the first rule for a detector. - isConditionsEnabled = this.partitioningFieldNames.length === 0; + return { + rule: { ...prevState.rule, actions }, + }; + }); + }; + + onConditionsEnabledChange = e => { + const isConditionsEnabled = e.target.checked; + this.setState(prevState => { + let conditions; + if (isConditionsEnabled === false) { + // Clear any conditions that have been added. + conditions = []; + } else { + // Add a default new condition. + conditions = [getNewConditionDefaults()]; } - this.setState({ - anomaly, - job, - ruleIndex, + return { + rule: { ...prevState.rule, conditions }, isConditionsEnabled, - isScopeEnabled: false, - isFlyoutVisible: true, - }); + }; + }); + }; - if (this.partitioningFieldNames.length > 0 && this.canGetFilters) { - // Load the current list of filters. These are used for configuring rule scope. - ml.filters - .filters() - .then(filters => { - const filterListIds = filters.map(filter => filter.filter_id); - this.setState({ - filterListIds, - }); - }) - .catch(resp => { - console.log('Error loading list of filters:', resp); - toastNotifications.addDanger( - intl.formatMessage({ - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithLoadingFilterListsNotificationMesssage', - defaultMessage: 'Error loading the filter lists used in the rule scope', - }) - ); - }); + addCondition = () => { + this.setState(prevState => { + const conditions = [...prevState.rule.conditions]; + conditions.push(getNewConditionDefaults()); + + return { + rule: { ...prevState.rule, conditions }, + }; + }); + }; + + updateCondition = (index, appliesTo, operator, value) => { + this.setState(prevState => { + const conditions = [...prevState.rule.conditions]; + if (index < conditions.length) { + conditions[index] = { + applies_to: appliesTo, + operator, + value, + }; } - }; - closeFlyout = () => { - this.setState({ isFlyoutVisible: false }); - }; + return { + rule: { ...prevState.rule, conditions }, + }; + }); + }; + + deleteCondition = index => { + this.setState(prevState => { + const conditions = [...prevState.rule.conditions]; + if (index < conditions.length) { + conditions.splice(index, 1); + } - setEditRuleIndex = ruleIndex => { - const detectorIndex = this.state.anomaly.detectorIndex; - const detector = this.state.job.analysis_config.detectors[detectorIndex]; - const rules = detector.custom_rules; - const rule = - rules === undefined || ruleIndex >= rules.length ? getNewRuleDefaults() : rules[ruleIndex]; - - const isConditionsEnabled = - this.partitioningFieldNames.length === 0 || - (rule.conditions !== undefined && rule.conditions.length > 0); - const isScopeEnabled = rule.scope !== undefined && Object.keys(rule.scope).length > 0; - if (isScopeEnabled === true) { - // Add 'enabled:true' to mark them as selected in the UI. - Object.keys(rule.scope).forEach(field => { - rule.scope[field].enabled = true; - }); + return { + rule: { ...prevState.rule, conditions }, + }; + }); + }; + + onScopeEnabledChange = e => { + const isScopeEnabled = e.target.checked; + this.setState(prevState => { + const rule = { ...prevState.rule }; + if (isScopeEnabled === false) { + // Clear scope property. + delete rule.scope; } - this.setState({ - ruleIndex, + return { rule, - isConditionsEnabled, isScopeEnabled, - }); - }; - - onSkipResultChange = e => { - const checked = e.target.checked; - this.setState(prevState => { - const actions = [...prevState.rule.actions]; - const idx = actions.indexOf(ACTION.SKIP_RESULT); - if (idx === -1 && checked) { - actions.push(ACTION.SKIP_RESULT); - } else if (idx > -1 && !checked) { - actions.splice(idx, 1); - } - - return { - rule: { ...prevState.rule, actions }, - }; - }); - }; - - onSkipModelUpdateChange = e => { - const checked = e.target.checked; - this.setState(prevState => { - const actions = [...prevState.rule.actions]; - const idx = actions.indexOf(ACTION.SKIP_MODEL_UPDATE); - if (idx === -1 && checked) { - actions.push(ACTION.SKIP_MODEL_UPDATE); - } else if (idx > -1 && !checked) { - actions.splice(idx, 1); - } - - return { - rule: { ...prevState.rule, actions }, - }; - }); - }; - - onConditionsEnabledChange = e => { - const isConditionsEnabled = e.target.checked; - this.setState(prevState => { - let conditions; - if (isConditionsEnabled === false) { - // Clear any conditions that have been added. - conditions = []; - } else { - // Add a default new condition. - conditions = [getNewConditionDefaults()]; - } - - return { - rule: { ...prevState.rule, conditions }, - isConditionsEnabled, - }; - }); - }; - - addCondition = () => { - this.setState(prevState => { - const conditions = [...prevState.rule.conditions]; - conditions.push(getNewConditionDefaults()); - - return { - rule: { ...prevState.rule, conditions }, - }; - }); - }; - - updateCondition = (index, appliesTo, operator, value) => { - this.setState(prevState => { - const conditions = [...prevState.rule.conditions]; - if (index < conditions.length) { - conditions[index] = { - applies_to: appliesTo, - operator, - value, - }; - } - - return { - rule: { ...prevState.rule, conditions }, - }; - }); - }; - - deleteCondition = index => { - this.setState(prevState => { - const conditions = [...prevState.rule.conditions]; - if (index < conditions.length) { - conditions.splice(index, 1); - } - - return { - rule: { ...prevState.rule, conditions }, - }; - }); - }; - - onScopeEnabledChange = e => { - const isScopeEnabled = e.target.checked; - this.setState(prevState => { - const rule = { ...prevState.rule }; - if (isScopeEnabled === false) { - // Clear scope property. - delete rule.scope; - } - - return { - rule, - isScopeEnabled, - }; - }); - }; - - updateScope = (fieldName, filterId, filterType, enabled) => { - this.setState(prevState => { - let scope = { ...prevState.rule.scope }; - if (scope === undefined) { - scope = {}; - } + }; + }); + }; + + updateScope = (fieldName, filterId, filterType, enabled) => { + this.setState(prevState => { + let scope = { ...prevState.rule.scope }; + if (scope === undefined) { + scope = {}; + } - scope[fieldName] = { - filter_id: filterId, - filter_type: filterType, - enabled, - }; + scope[fieldName] = { + filter_id: filterId, + filter_type: filterType, + enabled, + }; - return { - rule: { ...prevState.rule, scope }, - }; - }); - }; + return { + rule: { ...prevState.rule, scope }, + }; + }); + }; - saveEdit = () => { - const { rule, ruleIndex } = this.state; + saveEdit = () => { + const { rule, ruleIndex } = this.state; - this.updateRuleAtIndex(ruleIndex, rule); - }; + this.updateRuleAtIndex(ruleIndex, rule); + }; - updateRuleAtIndex = (ruleIndex, editedRule) => { - const { intl } = this.props; - const { job, anomaly } = this.state; + updateRuleAtIndex = (ruleIndex, editedRule) => { + const { toasts } = this.props.kibana.services.notifications; + const { job, anomaly } = this.state; - const jobId = job.job_id; - const detectorIndex = anomaly.detectorIndex; + const jobId = job.job_id; + const detectorIndex = anomaly.detectorIndex; - saveJobRule(job, detectorIndex, ruleIndex, editedRule) - .then(resp => { - if (resp.success) { - toastNotifications.add({ - title: intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.changesToJobDetectorRulesSavedNotificationMessageTitle', - defaultMessage: 'Changes to {jobId} detector rules saved', - }, - { jobId } - ), - color: 'success', - iconType: 'check', - text: intl.formatMessage({ - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.changesToJobDetectorRulesSavedNotificationMessageDescription', + saveJobRule(job, detectorIndex, ruleIndex, editedRule) + .then(resp => { + if (resp.success) { + toasts.add({ + title: i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.changesToJobDetectorRulesSavedNotificationMessageTitle', + { + defaultMessage: 'Changes to {jobId} detector rules saved', + values: { jobId }, + } + ), + color: 'success', + iconType: 'check', + text: i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.changesToJobDetectorRulesSavedNotificationMessageDescription', + { defaultMessage: 'Note that changes will take effect for new results only.', - }), - }); - this.closeFlyout(); - } else { - toastNotifications.addDanger( - intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithSavingChangesToJobDetectorRulesNotificationMessage', - defaultMessage: 'Error saving changes to {jobId} detector rules', - }, - { jobId } - ) - ); - } - }) - .catch(error => { - console.error(error); - toastNotifications.addDanger( - intl.formatMessage( + } + ), + }); + this.closeFlyout(); + } else { + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithSavingChangesToJobDetectorRulesNotificationMessage', { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithSavingChangesToJobDetectorRulesNotificationMessage', defaultMessage: 'Error saving changes to {jobId} detector rules', - }, - { jobId } + values: { jobId }, + } ) ); - }); - }; - - deleteRuleAtIndex = index => { - const { intl } = this.props; - const { job, anomaly } = this.state; - const jobId = job.job_id; - const detectorIndex = anomaly.detectorIndex; - - deleteJobRule(job, detectorIndex, index) - .then(resp => { - if (resp.success) { - toastNotifications.addSuccess( - intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.ruleDeletedFromJobDetectorNotificationMessage', - defaultMessage: 'Rule deleted from {jobId} detector', - }, - { jobId } - ) - ); - this.closeFlyout(); - } else { - toastNotifications.addDanger( - intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithDeletingRuleFromJobDetectorNotificationMessage', - defaultMessage: 'Error deleting rule from {jobId} detector', - }, - { jobId } - ) - ); - } - }) - .catch(error => { - console.error(error); - let errorMessage = intl.formatMessage( + } + }) + .catch(error => { + console.error(error); + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithSavingChangesToJobDetectorRulesNotificationMessage', { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithDeletingRuleFromJobDetectorNotificationMessage', - defaultMessage: 'Error deleting rule from {jobId} detector', - }, - { jobId } + defaultMessage: 'Error saving changes to {jobId} detector rules', + values: { jobId }, + } + ) + ); + }); + }; + + deleteRuleAtIndex = index => { + const { toasts } = this.props.kibana.services.notifications; + const { job, anomaly } = this.state; + const jobId = job.job_id; + const detectorIndex = anomaly.detectorIndex; + + deleteJobRule(job, detectorIndex, index) + .then(resp => { + if (resp.success) { + toasts.addSuccess( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.ruleDeletedFromJobDetectorNotificationMessage', + { + defaultMessage: 'Rule deleted from {jobId} detector', + values: { jobId }, + } + ) ); - if (error.message) { - errorMessage += ` : ${error.message}`; - } - toastNotifications.addDanger(errorMessage); - }); - }; - - addItemToFilterList = (item, filterId, closeFlyoutOnAdd) => { - const { intl } = this.props; - addItemToFilter(item, filterId) - .then(() => { - if (closeFlyoutOnAdd === true) { - toastNotifications.add({ - title: intl.formatMessage( - { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.addedItemToFilterListNotificationMessageTitle', - defaultMessage: 'Added {item} to {filterId}', - }, - { item, filterId } - ), - color: 'success', - iconType: 'check', - text: intl.formatMessage({ - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.addedItemToFilterListNotificationMessageDescription', - defaultMessage: 'Note that changes will take effect for new results only.', - }), - }); - this.closeFlyout(); - } - }) - .catch(error => { - console.log(`Error adding ${item} to filter ${filterId}:`, error); - toastNotifications.addDanger( - intl.formatMessage( + this.closeFlyout(); + } else { + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithDeletingRuleFromJobDetectorNotificationMessage', { - id: - 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithAddingItemToFilterListNotificationMessage', - defaultMessage: 'An error occurred adding {item} to filter {filterId}', - }, - { item, filterId } + defaultMessage: 'Error deleting rule from {jobId} detector', + values: { jobId }, + } ) ); - }); - }; - - render() { - const { intl } = this.props; - const { - isFlyoutVisible, - job, - anomaly, - ruleIndex, - rule, - filterListIds, - isConditionsEnabled, - isScopeEnabled, - } = this.state; - - if (isFlyoutVisible === false) { - return null; - } + } + }) + .catch(error => { + console.error(error); + let errorMessage = i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithDeletingRuleFromJobDetectorNotificationMessage', + { + defaultMessage: 'Error deleting rule from {jobId} detector', + values: { jobId }, + } + ); + if (error.message) { + errorMessage += ` : ${error.message}`; + } + toasts.addDanger(errorMessage); + }); + }; + + addItemToFilterList = (item, filterId, closeFlyoutOnAdd) => { + const { toasts } = this.props.kibana.services.notifications; + addItemToFilter(item, filterId) + .then(() => { + if (closeFlyoutOnAdd === true) { + toasts.add({ + title: i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.addedItemToFilterListNotificationMessageTitle', + { + defaultMessage: 'Added {item} to {filterId}', + values: { item, filterId }, + } + ), + color: 'success', + iconType: 'check', + text: i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.addedItemToFilterListNotificationMessageDescription', + { + defaultMessage: 'Note that changes will take effect for new results only.', + } + ), + }); + this.closeFlyout(); + } + }) + .catch(error => { + console.log(`Error adding ${item} to filter ${filterId}:`, error); + toasts.addDanger( + i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.errorWithAddingItemToFilterListNotificationMessage', + { + defaultMessage: 'An error occurred adding {item} to filter {filterId}', + values: { item, filterId }, + } + ) + ); + }); + }; + + render() { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = this.props.kibana.services.docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-rules.html`; + const { + isFlyoutVisible, + job, + anomaly, + ruleIndex, + rule, + filterListIds, + isConditionsEnabled, + isScopeEnabled, + } = this.state; + + if (isFlyoutVisible === false) { + return null; + } - let flyout; - - if (ruleIndex === -1) { - flyout = ( - - - -

+ let flyout; + + if (ruleIndex === -1) { + flyout = ( + + + +

+ +

+
+
+ + + + + + + + + -

-
-
- - - - - - - - - - - - - - -
- ); - } else { - const detectorIndex = anomaly.detectorIndex; - const detector = job.analysis_config.detectors[detectorIndex]; - const rules = detector.custom_rules; - const isCreate = rules === undefined || ruleIndex >= rules.length; - - const hasPartitioningFields = - this.partitioningFieldNames && this.partitioningFieldNames.length > 0; - const conditionSupported = - CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(anomaly.source.function) === -1; - const conditionsText = intl.formatMessage({ - id: 'xpack.ml.ruleEditor.ruleEditorFlyout.conditionsDescription', + + + + + + ); + } else { + const detectorIndex = anomaly.detectorIndex; + const detector = job.analysis_config.detectors[detectorIndex]; + const rules = detector.custom_rules; + const isCreate = rules === undefined || ruleIndex >= rules.length; + + const hasPartitioningFields = + this.partitioningFieldNames && this.partitioningFieldNames.length > 0; + const conditionSupported = + CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(anomaly.source.function) === -1; + const conditionsText = i18n.translate( + 'xpack.ml.ruleEditor.ruleEditorFlyout.conditionsDescription', + { defaultMessage: 'Add numeric conditions for when the rule applies. Multiple conditions are combined using AND.', - }); - - flyout = ( - - - -

- {isCreate === true ? ( - - ) : ( - - )} -

-
-
- - - - - -

+ } + ); + + flyout = ( + + + +

+ {isCreate === true ? ( - - - ), - }} + id="xpack.ml.ruleEditor.ruleEditorFlyout.createRuleTitle" + defaultMessage="Create rule" /> -

- - - - - -

+ ) : ( -

-
- + )} +

+ + + + + + + +

+ + + + ), + }} + /> +

+
- + - -

- -

-
- - {conditionSupported === true ? ( - +

+ - ) : ( - - } - iconType="iInCircle" +

+ + + + + + +

+ - )} - - - - - - + + + {conditionSupported === true ? ( + - + ) : ( } - color="warning" - iconType="help" - > -

+ iconType="iInCircle" + /> + )} + + + + + + + + + } + color="warning" + iconType="help" + > +

+ +

+

+ +

+
+ + + + + + -

-

+ + + + -

- - - - - - - - - - - - - - - - - - - ); - } - - return {flyout}; + +
+
+
+ + ); } + + return {flyout}; } -); +} + +export const RuleEditorFlyout = withKibana(RuleEditorFlyoutUI); diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js index c498a75fa2ec1..7259e4f7d5016 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js @@ -49,6 +49,12 @@ jest.mock('../../privilege/check_privilege', () => ({ checkPermission: () => true, })); +jest.mock('../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: comp => { + return comp; + }, +})); + import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; @@ -77,9 +83,17 @@ function prepareTest() { const requiredProps = { setShowFunction, unsetShowFunction, + kibana: { + services: { + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }, + }, + }, }; - const component = ; + const component = ; const wrapper = shallowWithIntl(component); diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap index d82f78cbc4e1a..b512f6d7c014c 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap @@ -17,7 +17,7 @@ exports[`RuleActionPanel renders panel for rule with a condition 1`] = ` />, }, Object { - "description": { - const enteredValue = event.target.value; - this.setState({ - value: enteredValue !== '' ? +enteredValue : '', - }); - }; + this.state = { value }; + } + + onChangeValue = event => { + const enteredValue = event.target.value; + this.setState({ + value: enteredValue !== '' ? +enteredValue : '', + }); + }; - onUpdateClick = () => { - const { conditionIndex, updateConditionValue } = this.props; - updateConditionValue(conditionIndex, this.state.value); - }; + onUpdateClick = () => { + const { conditionIndex, updateConditionValue } = this.props; + updateConditionValue(conditionIndex, this.state.value); + }; - render() { - const { intl } = this.props; - const value = this.state.value; - return ( - + render() { + const value = this.state.value; + return ( + + + + + + + + + + {value !== '' && ( - + this.onUpdateClick()}> - + - - - - {value !== '' && ( - - this.onUpdateClick()}> - - - - )} - - ); - } + )} + + ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js index b9027c932e302..5d8916cf22a12 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js @@ -31,7 +31,7 @@ function prepareTest(updateConditionValueFn, appliesTo) { updateConditionValue: updateConditionValueFn, }; - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); return wrapper; } diff --git a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js index a5ed7c3753b2f..98e027ec4f365 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -28,9 +28,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; -import { metadata } from 'ui/metadata'; -// metadata.branch corresponds to the version used in documentation links. -const jobTipsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/create-jobs.html#job-tips`; +import { getDocLinks } from '../../util/dependency_cache'; // don't use something like plugins/ml/../common // because it won't work with the jest tests @@ -253,6 +251,8 @@ export class ValidateJob extends Component { }; render() { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = getDocLinks(); + const jobTipsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#job-tips`; // only set to false if really false and not another falsy value, so it defaults to true. const fill = this.props.fill === false ? false : true; // default to false if not explicitly set to true diff --git a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js index 575320f728627..cc8a5abb4e9ab 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js @@ -9,6 +9,13 @@ import React from 'react'; import { ValidateJob } from './validate_job_view'; +jest.mock('../../util/dependency_cache', () => ({ + getDocLinks: () => ({ + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }), +})); + const job = { job_id: 'test-id', }; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts index 629e52797fb42..7ebbd45fd372a 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts @@ -4,12 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - KibanaContext, - KibanaContextValue, - SavedSearchQuery, - KibanaConfigTypeFix, -} from './kibana_context'; -export { useKibanaContext } from './use_kibana_context'; -export { useCurrentIndexPattern } from './use_current_index_pattern'; -export { useCurrentSavedSearch } from './use_current_saved_search'; +export { useMlKibana, StartServices, MlKibanaReactContextValue } from './kibana_context'; +export { useUiSettings } from './use_ui_settings_context'; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 9d0a3bc43e258..aaf539322809b 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -4,43 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - -import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { CoreStart } from 'kibana/public'; import { - IndexPattern, - IndexPatternsContract, -} from '../../../../../../../../src/plugins/data/public'; -import { SavedSearchSavedObject } from '../../../../common/types/kibana'; - -// set() method is missing in original d.ts -export interface KibanaConfigTypeFix extends KibanaConfig { - set(key: string, value: any): void; -} + useKibana, + KibanaReactContextValue, +} from '../../../../../../../../src/plugins/kibana_react/public'; -export interface KibanaContextValue { - combinedQuery: any; - currentIndexPattern: IndexPattern; // TODO this should be IndexPattern or null - currentSavedSearch: SavedSearchSavedObject | null; - indexPatterns: IndexPatternsContract; - kibanaConfig: KibanaConfigTypeFix; +interface StartPlugins { + data: DataPublicPluginStart; } - -export type SavedSearchQuery = object; - -// This context provides dependencies which can be injected -// via angularjs only (like services, currentIndexPattern etc.). -// Because we cannot just import these dependencies, the default value -// for the context is just {} and of type `Partial` -// for the angularjs based dependencies. Therefore, the -// actual dependencies are set like we did previously with KibanaContext -// in the wrapping angularjs directive. In the custom hook we check if -// the dependencies are present with error reporting if they weren't -// added properly. That's why in tests, these custom hooks must not -// be mocked, instead ` needs -// to be used. This guarantees that we have both properly set up -// TypeScript support and runtime checks for these dependencies. -// Multiple custom hooks can be created to access subsets of -// the overall context value if necessary too, -// see useCurrentIndexPattern() for example. -export const KibanaContext = React.createContext>({}); +export type StartServices = CoreStart & StartPlugins; +// eslint-disable-next-line react-hooks/rules-of-hooks +export const useMlKibana = () => useKibana(); +export type MlKibanaReactContextValue = KibanaReactContextValue; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_ui_settings_context.ts similarity index 64% rename from x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_ui_settings_context.ts index 4964d727a0452..92f59f62f8a25 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_ui_settings_context.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiChromeMock } from './mocks_jest'; +import { useMlKibana } from './kibana_context'; -export const useUiChromeContext = () => uiChromeMock; +export const useUiSettings = () => { + return useMlKibana().services.uiSettings; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_pattern.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_pattern.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/index_pattern.ts diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_patterns.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/index_patterns.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_patterns.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/index_patterns.ts diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_config.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/kibana_config.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_config.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/kibana_config.ts diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context_value.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/kibana_context_value.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context_value.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/kibana_context_value.ts diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/saved_search.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/__mocks__/saved_search.ts diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/index.ts similarity index 66% rename from x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/index.ts index 46178a7d02977..7b48d717ea190 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/timefilter', () => { - return {}; -}); +export { MlContext, MlContextValue, SavedSearchQuery } from './ml_context'; +export { useMlContext } from './use_ml_context'; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ml/ml_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/ml_context.ts new file mode 100644 index 0000000000000..6b6c34dd37968 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/ml_context.ts @@ -0,0 +1,39 @@ +/* + * 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 { + IndexPattern, + IndexPatternsContract, +} from '../../../../../../../../src/plugins/data/public'; +import { SavedSearchSavedObject } from '../../../../common/types/kibana'; + +export interface MlContextValue { + combinedQuery: any; + currentIndexPattern: IndexPattern; // TODO this should be IndexPattern or null + currentSavedSearch: SavedSearchSavedObject | null; + indexPatterns: IndexPatternsContract; + kibanaConfig: any; // IUiSettingsClient; +} + +export type SavedSearchQuery = object; + +// This context provides dependencies which can be injected +// via angularjs only (like services, currentIndexPattern etc.). +// Because we cannot just import these dependencies, the default value +// for the context is just {} and of type `Partial` +// for the angularjs based dependencies. Therefore, the +// actual dependencies are set like we did previously with KibanaContext +// in the wrapping angularjs directive. In the custom hook we check if +// the dependencies are present with error reporting if they weren't +// added properly. That's why in tests, these custom hooks must not +// be mocked, instead ` needs +// to be used. This guarantees that we have both properly set up +// TypeScript support and runtime checks for these dependencies. +// Multiple custom hooks can be created to access subsets of +// the overall context value if necessary too, +// see useCurrentIndexPattern() for example. +export const MlContext = React.createContext>({}); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_index_pattern.ts similarity index 83% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_index_pattern.ts index 62be409882dff..4469deae4d15e 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_index_pattern.ts @@ -6,10 +6,10 @@ import { useContext } from 'react'; -import { KibanaContext } from './kibana_context'; +import { MlContext } from './ml_context'; export const useCurrentIndexPattern = () => { - const context = useContext(KibanaContext); + const context = useContext(MlContext); if (context.currentIndexPattern === undefined) { throw new Error('currentIndexPattern is undefined'); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_saved_search.ts similarity index 83% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_saved_search.ts index 1147b905f237e..d31d9dd5bead9 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_current_saved_search.ts @@ -6,10 +6,10 @@ import { useContext } from 'react'; -import { KibanaContext } from './kibana_context'; +import { MlContext } from './ml_context'; export const useCurrentSavedSearch = () => { - const context = useContext(KibanaContext); + const context = useContext(MlContext); if (context.currentSavedSearch === undefined) { throw new Error('currentSavedSearch is undefined'); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_ml_context.ts similarity index 74% rename from x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ml/use_ml_context.ts index 658a6980aa1ae..c8bf54309bd9e 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/ml/use_ml_context.ts @@ -6,10 +6,10 @@ import { useContext } from 'react'; -import { KibanaContext, KibanaContextValue } from './kibana_context'; +import { MlContext, MlContextValue } from './ml_context'; -export const useKibanaContext = () => { - const context = useContext(KibanaContext); +export const useMlContext = () => { + const context = useContext(MlContext); if ( context.combinedQuery === undefined || @@ -21,5 +21,5 @@ export const useKibanaContext = () => { throw new Error('required attribute is undefined'); } - return context as KibanaContextValue; + return context as MlContextValue; }; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_jest.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_jest.ts deleted file mode 100644 index 785daec0ab369..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_jest.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const uiChromeMock = { - getBasePath: () => 'basePath', - getUiSettingsClient: () => { - return { - get: (key: string) => { - switch (key) { - case 'dateFormat': - return 'MMM D, YYYY @ HH:mm:ss.SSS'; - case 'theme:darkMode': - return false; - case 'timepicker:timeDefaults': - return {}; - case 'timepicker:refreshIntervalDefaults': - return { pause: false, value: 0 }; - default: - throw new Error(`Unexpected config key: ${key}`); - } - }, - }; - }, -}; - -interface RefreshInterval { - value: number; - pause: boolean; -} - -const time = { - from: 'Thu Aug 29 2019 02:04:19 GMT+0200', - to: 'Sun Sep 29 2019 01:45:36 GMT+0200', -}; - -export const uiTimefilterMock = { - isAutoRefreshSelectorEnabled() { - return this._isAutoRefreshSelectorEnabled; - }, - isTimeRangeSelectorEnabled() { - return this._isTimeRangeSelectorEnabled; - }, - enableAutoRefreshSelector() { - this._isAutoRefreshSelectorEnabled = true; - }, - enableTimeRangeSelector() { - this._isTimeRangeSelectorEnabled = true; - }, - getEnabledUpdated$() { - return { subscribe: jest.fn() }; - }, - getRefreshInterval() { - return this.refreshInterval; - }, - getRefreshIntervalUpdate$() { - return { subscribe: jest.fn() }; - }, - getTime: () => time, - getTimeUpdate$() { - return { subscribe: jest.fn() }; - }, - _isAutoRefreshSelectorEnabled: false, - _isTimeRangeSelectorEnabled: false, - refreshInterval: { value: 0, pause: true }, - on: (event: string, reload: () => void) => {}, - setRefreshInterval(refreshInterval: RefreshInterval) { - this.refreshInterval = refreshInterval; - }, -}; - -export const uiTimeHistoryMock = { - get: () => [time], -}; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts deleted file mode 100644 index cd3d80bed8d14..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks_mocha.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Subject } from 'rxjs'; - -export const uiChromeMock = { - getBasePath: () => 'basePath', - getUiSettingsClient: () => { - return { - get: (key: string) => { - switch (key) { - case 'dateFormat': - return 'MMM D, YYYY @ HH:mm:ss.SSS'; - case 'theme:darkMode': - return false; - case 'timepicker:timeDefaults': - return {}; - case 'timepicker:refreshIntervalDefaults': - return { pause: false, value: 0 }; - default: - throw new Error(`Unexpected config key: ${key}`); - } - }, - }; - }, -}; - -interface RefreshInterval { - value: number; - pause: boolean; -} - -const time = { - from: 'Thu Aug 29 2019 02:04:19 GMT+0200', - to: 'Sun Sep 29 2019 01:45:36 GMT+0200', -}; - -export const uiTimefilterMock = { - isAutoRefreshSelectorEnabled() { - return this._isAutoRefreshSelectorEnabled; - }, - isTimeRangeSelectorEnabled() { - return this._isTimeRangeSelectorEnabled; - }, - enableAutoRefreshSelector() { - this._isAutoRefreshSelectorEnabled = true; - }, - enableTimeRangeSelector() { - this._isTimeRangeSelectorEnabled = true; - }, - getActiveBounds() { - return; - }, - getEnabledUpdated$() { - return { subscribe: () => {} }; - }, - getFetch$() { - return new Subject(); - }, - getRefreshInterval() { - return this.refreshInterval; - }, - getRefreshIntervalUpdate$() { - return { subscribe: () => {} }; - }, - getTime: () => time, - getTimeUpdate$() { - return { subscribe: () => {} }; - }, - _isAutoRefreshSelectorEnabled: false, - _isTimeRangeSelectorEnabled: false, - refreshInterval: { value: 0, pause: true }, - on: (event: string, reload: () => void) => {}, - setRefreshInterval(refreshInterval: RefreshInterval) { - this.refreshInterval = refreshInterval; - }, -}; - -export const uiTimeHistoryMock = { - get: () => [time], -}; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts deleted file mode 100644 index 0aaaa868c490a..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { uiChromeMock, uiTimefilterMock, uiTimeHistoryMock } from './mocks_jest'; - -export const useUiContext = () => ({ - chrome: uiChromeMock, - timefilter: uiTimefilterMock, - timeHistory: uiTimeHistoryMock, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts deleted file mode 100644 index 18cbb49181e38..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// We only export UiContext but not any custom hooks, because if we'd import them -// from here, mocking the hook from jest tests won't work as expected. -export { UiContext } from './ui_context'; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx b/x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx deleted file mode 100644 index 4cb97cf5639fe..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import chrome from 'ui/chrome'; -import { timefilter, timeHistory } from 'ui/timefilter'; - -// This provides ui/* based imports via React Context. -// Because these dependencies can use regular imports, -// they are just passed on as the default value -// of the Context which means it's not necessary -// to add ... to the -// wrapping angular directive, reducing a lot of boilerplate. -// The custom hooks like useUiContext() need to be mocked in -// tests because we rely on the properly set up default value. -// Different custom hooks can be created to access parts only -// from the full context value, see useUiChromeContext() as an example. -export const UiContext = React.createContext({ - chrome, - timefilter, - timeHistory, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.ts deleted file mode 100644 index 1765bdb23df7f..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useContext } from 'react'; - -import { UiContext } from './ui_context'; - -export const useUiChromeContext = () => { - return useContext(UiContext).chrome; -}; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts deleted file mode 100644 index 156a42d9f3c50..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useContext } from 'react'; - -import { UiContext } from './ui_context'; - -export const useUiContext = () => { - return useContext(UiContext); -}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts index 924e1228c27ab..9182487cedb51 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts @@ -5,7 +5,6 @@ */ import { getAnalysisType, isOutlierAnalysis } from './analytics'; -jest.mock('ui/new_platform'); describe('Data Frame Analytics: Analytics utils', () => { test('getAnalysisType()', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 12d441a9a23ec..f87578c4bce48 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -12,7 +12,7 @@ import { cloneDeep } from 'lodash'; import { ml } from '../../services/ml_api_service'; import { Dictionary } from '../../../../common/types/common'; import { getErrorMessage } from '../pages/analytics_management/hooks/use_create_analytics_form'; -import { SavedSearchQuery } from '../../contexts/kibana'; +import { SavedSearchQuery } from '../../contexts/ml'; import { SortDirection } from '../../components/ml_in_memory_table'; export type IndexName = string; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 95e1b15d548c1..df2ca3e7de657 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -17,7 +17,7 @@ import { LoadingPanel } from '../loading_panel'; import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; interface GetDataFrameAnalyticsResponse { count: number; @@ -64,7 +64,7 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { undefined ); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const loadJobConfig = async () => { setIsLoadingJobConfig(true); @@ -107,7 +107,7 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { try { const sourceIndex = jobConfig.source.index[0]; const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId); + const indexPattern: IIndexPattern = await mlContext.indexPatterns.get(indexPatternId); if (indexPattern !== undefined) { await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false); } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 1e24bfec6de5e..23dd1ae288d8e 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -19,7 +19,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { ErrorCallout } from '../error_callout'; import { getDependentVar, @@ -50,6 +50,9 @@ interface Props { } export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { + const { + services: { docLinks }, + } = useMlKibana(); const [isLoading, setIsLoading] = useState(false); const [confusionMatrixData, setConfusionMatrixData] = useState([]); const [columns, setColumns] = useState([]); @@ -217,6 +220,8 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) return ; } + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + return ( = ({ jobConfig, jobStatus, searchQuery }) iconType="help" iconSide="left" color="primary" - href={`https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-dfanalytics-evaluate.html#ml-dfanalytics-classification`} + href={`${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-classification`} > {i18n.translate( 'xpack.ml.dataframe.analytics.classificationExploration.classificationDocsLink', diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx index 85794cf813ab5..849a0793a094b 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx @@ -39,7 +39,7 @@ import { import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; import { Field } from '../../../../../../../common/types/fields'; -import { SavedSearchQuery } from '../../../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES, diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx index 013ea8ddc78a5..ca8fd68079f7e 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx @@ -7,11 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; - -jest.mock('../../../../../contexts/ui/use_ui_chrome_context'); -jest.mock('ui/new_platform'); +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { Exploration } from './exploration'; @@ -24,9 +21,9 @@ jest.mock('react', () => { describe('Data Frame Analytics: ', () => { test('Minimal initialization', () => { const wrapper = shallow( - + - + ); // Without the jobConfig being loaded, the component will just return empty. expect(wrapper.text()).toMatch(''); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx index bd1b60d92403e..ce72e90b4c230 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx @@ -64,11 +64,11 @@ import { Query as QueryType, } from '../../../analytics_management/components/analytics_list/common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { SavedSearchQuery } from '../../../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; const FEATURE_INFLUENCE = 'feature_influence'; @@ -115,13 +115,13 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const [searchError, setSearchError] = useState(undefined); const [searchString, setSearchString] = useState(undefined); - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const initializeJobCapsService = async () => { if (jobConfig !== undefined) { const sourceIndex = jobConfig.source.index[0]; const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId); + const indexPattern: IIndexPattern = await mlContext.indexPatterns.get(indexPatternId); if (indexPattern !== undefined) { await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false); } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index fe2676053dde3..74937bf761285 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -16,7 +16,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { ErrorCallout } from '../error_callout'; import { getValuesFromResponse, @@ -46,6 +46,10 @@ interface Props { const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const [trainingEval, setTrainingEval] = useState(defaultEval); const [generalizationEval, setGeneralizationEval] = useState(defaultEval); const [isLoadingTraining, setIsLoadingTraining] = useState(false); @@ -256,7 +260,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) iconType="help" iconSide="left" color="primary" - href={`https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`} + href={`${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`} > {i18n.translate( 'xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink', diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 7399828bcd642..569cf21792874 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -17,7 +17,7 @@ import { LoadingPanel } from '../loading_panel'; import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; interface GetDataFrameAnalyticsResponse { count: number; @@ -64,7 +64,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { undefined ); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const loadJobConfig = async () => { setIsLoadingJobConfig(true); @@ -98,7 +98,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { try { const sourceIndex = jobConfig.source.index[0]; const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId); + const indexPattern: IIndexPattern = await mlContext.indexPatterns.get(indexPatternId); if (indexPattern !== undefined) { await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false); } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index 971fa99f2e93f..118652318785d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -39,7 +39,7 @@ import { import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; import { Field } from '../../../../../../../common/types/fields'; -import { SavedSearchQuery } from '../../../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES, diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx index 2a939d93a48b3..08cc54ec39c6f 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx @@ -18,7 +18,6 @@ jest.mock('../../../../../privilege/check_privilege', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), })); -jest.mock('ui/new_platform'); describe('DeleteAction', () => { test('When canDeleteDataFrameAnalytics permission is false, button should be disabled.', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts index 30f87ad8a375b..19a3857f3f71c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts @@ -5,7 +5,6 @@ */ import StatsMock from './__mocks__/analytics_stats.json'; -jest.mock('ui/new_platform'); import { isCompletedAnalyticsJob, diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts index 4ccfa8a562c6c..0e32bdb39e690 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts @@ -6,7 +6,7 @@ import React, { useEffect } from 'react'; -import { timefilter } from 'ui/timefilter'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { DEFAULT_REFRESH_INTERVAL_MS, @@ -18,6 +18,9 @@ import { useRefreshAnalyticsList } from '../../../../common'; export const useRefreshInterval = ( setBlockRefresh: React.Dispatch> ) => { + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; + const { refresh } = useRefreshAnalyticsList(); useEffect(() => { let analyticsRefreshInterval: null | number = null; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx index abb35e50ec2a2..7d58f0df12e6c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx @@ -10,8 +10,8 @@ import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpe import { CreateAnalyticsButton } from './create_analytics_button'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; @@ -19,7 +19,7 @@ const getMountedHook = () => mountHook( () => useCreateAnalyticsForm(), ({ children }) => ( - {children} + {children} ) ); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx index d5d509826667c..cacb3744f7ab4 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx @@ -10,8 +10,8 @@ import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpe import { CreateAnalyticsFlyout } from './create_analytics_flyout'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; @@ -19,7 +19,7 @@ const getMountedHook = () => mountHook( () => useCreateAnalyticsForm(), ({ children }) => ( - {children} + {children} ) ); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx index d01bae9616708..af6dadf236932 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx @@ -10,8 +10,8 @@ import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpe import { CreateAnalyticsForm } from './create_analytics_form'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; @@ -19,7 +19,7 @@ const getMountedHook = () => mountHook( () => useCreateAnalyticsForm(), ({ children }) => ( - {children} + {children} ) ); @@ -29,14 +29,27 @@ jest.mock('react', () => { return { ...r, memo: (x: any) => x }; }); +jest.mock('../../../../../contexts/kibana', () => ({ + useMlKibana: () => { + return { + services: { + docLinks: () => ({ + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }), + }, + }; + }, +})); + describe('Data Frame Analytics: ', () => { test('Minimal initialization', () => { const { getLastHookValue } = getMountedHook(); const props = getLastHookValue(); const wrapper = mount( - + - + ); const euiFormRows = wrapper.find('EuiFormRow'); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index e68523733254e..338fa1e4ac328 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -21,11 +21,11 @@ import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { metadata } from 'ui/metadata'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { ml } from '../../../../../services/ml_api_service'; import { Field } from '../../../../../../../common/types/fields'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; import { JOB_TYPES, @@ -45,8 +45,12 @@ import { DfAnalyticsExplainResponse, FieldSelectionItem } from '../../../../comm import { shouldAddAsDepVarOption, OMIT_FIELDS } from './form_options_validation'; export const CreateAnalyticsForm: FC = ({ actions, state }) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const { setFormState } = actions; - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const { form, indexPatternsMap, isAdvancedEditorEnabled, isJobCreated, requestMessages } = state; const { @@ -92,7 +96,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta // that an analytics jobs is not able to identify outliers if there are no numeric fields present. const validateSourceIndexFields = async () => { try { - const indexPattern: IndexPattern = await kibanaContext.indexPatterns.get( + const indexPattern: IndexPattern = await mlContext.indexPatterns.get( indexPatternsMap[sourceIndex].value ); const containsNumericalFields: boolean = indexPattern.fields.some( @@ -207,7 +211,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta sourceIndexContainsNumericalFields: true, }); try { - const indexPattern: IndexPattern = await kibanaContext.indexPatterns.get( + const indexPattern: IndexPattern = await mlContext.indexPatterns.get( indexPatternsMap[sourceIndex].value ); @@ -456,7 +460,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta )}
{i18n.translate( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx index 3298a7d00253f..2bdcc28e31fff 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { mountHook } from 'test_utils/enzyme_helpers'; -import { KibanaContext } from '../../../../../contexts/kibana'; -import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; +import { MlContext } from '../../../../../contexts/ml'; +import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { getErrorMessage, useCreateAnalyticsForm } from './use_create_analytics_form'; @@ -16,7 +16,7 @@ const getMountedHook = () => mountHook( () => useCreateAnalyticsForm(), ({ children }) => ( - {children} + {children} ) ); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index b2f9442f48edb..59474b63213a2 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { SimpleSavedObject } from 'src/core/public'; import { ml } from '../../../../../services/ml_api_service'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; import { useRefreshAnalyticsList, @@ -43,7 +43,7 @@ export function getErrorMessage(error: any) { } export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const [state, dispatch] = useReducer(reducer, getInitialState()); const { refresh } = useRefreshAnalyticsList(); @@ -130,7 +130,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const indexPatternName = destinationIndex; try { - const newIndexPattern = await kibanaContext.indexPatterns.make(); + const newIndexPattern = await mlContext.indexPatterns.make(); Object.assign(newIndexPattern, { id: '', @@ -161,8 +161,8 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { // check if there's a default index pattern, if not, // set the newly created one as the default index pattern. - if (!kibanaContext.kibanaConfig.get('defaultIndex')) { - await kibanaContext.kibanaConfig.set('defaultIndex', id); + if (!mlContext.kibanaConfig.get('defaultIndex')) { + await mlContext.kibanaConfig.set('defaultIndex', id); } addRequestMessage({ @@ -226,7 +226,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { try { // Set the index pattern titles which the user can choose as the source. const indexPatternsMap: SourceIndexMap = {}; - const savedObjects = (await kibanaContext.indexPatterns.getCache()) || []; + const savedObjects = (await mlContext.indexPatterns.getCache()) || []; savedObjects.forEach((obj: SimpleSavedObject>) => { const title = obj?.attributes?.title; if (title !== undefined) { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index fb366b517f0b7..3c0c3fa0df87c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; @@ -16,6 +16,7 @@ import { } from '../../components/analytics_list/common'; export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { + const toastNotifications = getToastNotifications(); try { if (isDataFrameAnalyticsFailed(d.stats.state)) { await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true, true); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts index da09c4842b843..6513cad808485 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; @@ -13,6 +13,7 @@ import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../.. import { DataFrameAnalyticsListRow } from '../../components/analytics_list/common'; export const startAnalytics = async (d: DataFrameAnalyticsListRow) => { + const toastNotifications = getToastNotifications(); try { await ml.dataFrameAnalytics.startDataFrameAnalytics(d.config.id); toastNotifications.addSuccess( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts index 84d1835c6e1e3..c92c03c3b0f16 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; @@ -16,6 +16,7 @@ import { } from '../../components/analytics_list/common'; export const stopAnalytics = async (d: DataFrameAnalyticsListRow) => { + const toastNotifications = getToastNotifications(); try { await ml.dataFrameAnalytics.stopDataFrameAnalytics( d.config.id, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 7c0bcac039164..ae0c034f972d6 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -22,8 +22,8 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { timefilter } from 'ui/timefilter'; import { isFullLicense } from '../license/check_license'; +import { useMlKibana } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; @@ -49,6 +49,8 @@ function startTrialDescription() { } export const DatavisualizerSelector: FC = () => { + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js index 4fe4933261985..99cdc816dfe3d 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { @@ -22,7 +23,7 @@ import { import { WelcomeContent } from './welcome_content'; -export const AboutPanel = injectI18n(function AboutPanel({ onFilePickerChange, intl }) { +export function AboutPanel({ onFilePickerChange }) { return ( @@ -36,10 +37,12 @@ export const AboutPanel = injectI18n(function AboutPanel({ onFilePickerChange, i
onFilePickerChange(files)} className="file-datavisualizer-file-picker" /> @@ -51,7 +54,7 @@ export const AboutPanel = injectI18n(function AboutPanel({ onFilePickerChange, i ); -}); +} export function LoadingPanel() { return ( diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js index 40bf7a8ff5f21..516ac791fc677 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js @@ -7,7 +7,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import React, { Component } from 'react'; -import { metadata } from 'ui/metadata'; import { EuiComboBox, @@ -31,6 +30,7 @@ import { // getCharsetOptions, } from './options'; import { isTimestampFormatValid } from './overrides_validation'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { TIMESTAMP_OPTIONS, CUSTOM_DROPDOWN_OPTION } from './options/option_lists'; @@ -43,7 +43,7 @@ const quoteOptions = getQuoteOptions(); const LINES_TO_SAMPLE_VALUE_MIN = 3; const LINES_TO_SAMPLE_VALUE_MAX = 1000000; -export class Overrides extends Component { +class OverridesUI extends Component { constructor(props) { super(props); @@ -268,8 +268,8 @@ export class Overrides extends Component { const fieldOptions = getSortedFields(fields); const timestampFormatErrorsList = [this.customTimestampFormatErrors, timestampFormatError]; - // metadata.branch corresponds to the version used in documentation links. - const docsUrl = `https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/search-aggregations-bucket-daterange-aggregation.html#date-format-pattern`; + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = this.props.kibana.services.docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/search-aggregations-bucket-daterange-aggregation.html#date-format-pattern`; const timestampFormatHelp = ( @@ -504,6 +504,8 @@ export class Overrides extends Component { } } +export const Overrides = withKibana(OverridesUI); + function selectedOption(opt) { return [{ label: opt || '' }]; } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.test.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.test.js index 9a66439adf697..ee0df7c9ab32e 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.test.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.test.js @@ -9,6 +9,12 @@ import React from 'react'; import { Overrides } from './overrides'; +jest.mock('../../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: comp => { + return comp; + }, +})); + function getProps() { return { setOverrides: () => {}, @@ -17,6 +23,14 @@ function getProps() { defaultSettings: {}, setApplyOverrides: () => {}, fields: [], + kibana: { + services: { + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }, + }, + }, }; } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/import_progress.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/import_progress.js index 324e64a674551..272ec2979ad2f 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/import_progress.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/import_progress.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { EuiStepsHorizontal, EuiProgress, EuiSpacer } from '@elastic/eui'; @@ -15,7 +16,7 @@ export const IMPORT_STATUS = { FAILED: 'danger', }; -export const ImportProgress = injectI18n(function({ statuses, intl }) { +export function ImportProgress({ statuses }) { const { reading, readStatus, @@ -63,26 +64,36 @@ export const ImportProgress = injectI18n(function({ statuses, intl }) { completedStep = 5; } - let processFileTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.processFileTitle', - defaultMessage: 'Process file', - }); - let createIndexTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.createIndexTitle', - defaultMessage: 'Create index', - }); - let createIngestPipelineTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.createIngestPipelineTitle', - defaultMessage: 'Create ingest pipeline', - }); - let uploadingDataTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.uploadDataTitle', - defaultMessage: 'Upload data', - }); - let createIndexPatternTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.createIndexPatternTitle', - defaultMessage: 'Create index pattern', - }); + let processFileTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.processFileTitle', + { + defaultMessage: 'Process file', + } + ); + let createIndexTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.createIndexTitle', + { + defaultMessage: 'Create index', + } + ); + let createIngestPipelineTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.createIngestPipelineTitle', + { + defaultMessage: 'Create ingest pipeline', + } + ); + let uploadingDataTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.uploadDataTitle', + { + defaultMessage: 'Upload data', + } + ); + let createIndexPatternTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.createIndexPatternTitle', + { + defaultMessage: 'Create index pattern', + } + ); const creatingIndexStatus = (

@@ -103,10 +114,12 @@ export const ImportProgress = injectI18n(function({ statuses, intl }) { ); if (completedStep >= 0) { - processFileTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.processingFileTitle', - defaultMessage: 'Processing file', - }); + processFileTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.processingFileTitle', + { + defaultMessage: 'Processing file', + } + ); statusInfo = (

= 1) { - processFileTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.fileProcessedTitle', - defaultMessage: 'File processed', - }); - createIndexTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.creatingIndexTitle', - defaultMessage: 'Creating index', - }); + processFileTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.fileProcessedTitle', + { + defaultMessage: 'File processed', + } + ); + createIndexTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.creatingIndexTitle', + { + defaultMessage: 'Creating index', + } + ); statusInfo = createPipeline === true ? creatingIndexAndIngestPipelineStatus : creatingIndexStatus; } if (completedStep >= 2) { - createIndexTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.indexCreatedTitle', - defaultMessage: 'Index created', - }); - createIngestPipelineTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.creatingIngestPipelineTitle', - defaultMessage: 'Creating ingest pipeline', - }); + createIndexTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.indexCreatedTitle', + { + defaultMessage: 'Index created', + } + ); + createIngestPipelineTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.creatingIngestPipelineTitle', + { + defaultMessage: 'Creating ingest pipeline', + } + ); statusInfo = createPipeline === true ? creatingIndexAndIngestPipelineStatus : creatingIndexStatus; } if (completedStep >= 3) { - createIngestPipelineTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.ingestPipelineCreatedTitle', - defaultMessage: 'Ingest pipeline created', - }); - uploadingDataTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.uploadingDataTitle', - defaultMessage: 'Uploading data', - }); + createIngestPipelineTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.ingestPipelineCreatedTitle', + { + defaultMessage: 'Ingest pipeline created', + } + ); + uploadingDataTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.uploadingDataTitle', + { + defaultMessage: 'Uploading data', + } + ); statusInfo = ; } if (completedStep >= 4) { - uploadingDataTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.dataUploadedTitle', - defaultMessage: 'Data uploaded', - }); + uploadingDataTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.dataUploadedTitle', + { + defaultMessage: 'Data uploaded', + } + ); if (createIndexPattern === true) { - createIndexPatternTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.creatingIndexPatternTitle', - defaultMessage: 'Creating index pattern', - }); + createIndexPatternTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.creatingIndexPatternTitle', + { + defaultMessage: 'Creating index pattern', + } + ); statusInfo = (

= 5) { - createIndexPatternTitle = intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importProgress.indexPatternCreatedTitle', - defaultMessage: 'Index pattern created', - }); + createIndexPatternTitle = i18n.translate( + 'xpack.ml.fileDatavisualizer.importProgress.indexPatternCreatedTitle', + { + defaultMessage: 'Index pattern created', + } + ); statusInfo = null; } @@ -240,7 +271,7 @@ export const ImportProgress = injectI18n(function({ statuses, intl }) { )} ); -}); +} function UploadFunctionProgress({ progress }) { return ( diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js index 2d431cc046462..94143ea354d70 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { @@ -19,7 +20,7 @@ import { import { MLJobEditor, EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor'; const EDITOR_HEIGHT = '300px'; -function AdvancedSettingsUi({ +export function AdvancedSettings({ index, indexPattern, initialized, @@ -35,7 +36,6 @@ function AdvancedSettingsUi({ onPipelineStringChange, indexNameError, indexPatternNameError, - intl, }) { return ( @@ -50,18 +50,22 @@ function AdvancedSettingsUi({ error={[indexNameError]} > @@ -131,8 +135,6 @@ function AdvancedSettingsUi({ ); } -export const AdvancedSettings = injectI18n(AdvancedSettingsUi); - function IndexSettings({ initialized, data, onChange }) { return ( diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/import_settings.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/import_settings.js index 4d066fa84f070..ba637c472333d 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/import_settings.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/import_settings.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiTabbedContent, EuiSpacer } from '@elastic/eui'; @@ -12,7 +12,7 @@ import { EuiTabbedContent, EuiSpacer } from '@elastic/eui'; import { SimpleSettings } from './simple'; import { AdvancedSettings } from './advanced'; -export const ImportSettings = injectI18n(function({ +export const ImportSettings = ({ index, indexPattern, initialized, @@ -28,13 +28,11 @@ export const ImportSettings = injectI18n(function({ onPipelineStringChange, indexNameError, indexPatternNameError, - intl, -}) { +}) => { const tabs = [ { id: 'simple-settings', - name: intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importSettings.simpleTabName', + name: i18n.translate('xpack.ml.fileDatavisualizer.importSettings.simpleTabName', { defaultMessage: 'Simple', }), content: ( @@ -54,8 +52,7 @@ export const ImportSettings = injectI18n(function({ }, { id: 'advanced-settings', - name: intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.importSettings.advancedTabName', + name: i18n.translate('xpack.ml.fileDatavisualizer.importSettings.advancedTabName', { defaultMessage: 'Advanced', }), content: ( @@ -88,4 +85,4 @@ export const ImportSettings = injectI18n(function({ {}} /> ); -}); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js index beee48d8cc577..8c6f569bf8605 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js @@ -4,20 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { EuiFieldText, EuiFormRow, EuiCheckbox, EuiSpacer } from '@elastic/eui'; -export const SimpleSettings = injectI18n(function({ +export const SimpleSettings = ({ index, initialized, onIndexChange, createIndexPattern, onCreateIndexPatternChange, indexNameError, - intl, -}) { +}) => { return ( @@ -62,4 +66,4 @@ export const SimpleSettings = injectI18n(function({ /> ); -}); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js index f1cc456ae4de8..aaebca2f58963 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js @@ -10,15 +10,16 @@ import React, { Component } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import moment from 'moment'; -import uiChrome from 'ui/chrome'; + import { ml } from '../../../../services/ml_api_service'; import { isFullLicense } from '../../../../license/check_license'; import { checkPermission } from '../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; const RECHECK_DELAY_MS = 3000; -export class ResultsLinks extends Component { +class ResultsLinksUI extends Component { constructor(props) { super(props); @@ -76,6 +77,7 @@ export class ResultsLinks extends Component { ? `&_g=(time:(from:'${from}',mode:quick,to:'${to}'))` : ''; + const { basePath } = this.props.kibana.services.http; return ( {createIndexPattern && ( @@ -89,7 +91,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/kibana#/discover?&_a=(index:'${indexPatternId}')${_g}`} + href={`${basePath.get()}/app/kibana#/discover?&_a=(index:'${indexPatternId}')${_g}`} /> )} @@ -139,7 +141,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/kibana#/management/elasticsearch/index_management/indices/filter/${index}`} + href={`${basePath.get()}/app/kibana#/management/elasticsearch/index_management/indices/filter/${index}`} /> @@ -153,7 +155,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/kibana#/management/kibana/index_patterns/${ + href={`${basePath.get()}/app/kibana#/management/kibana/index_patterns/${ createIndexPattern ? indexPatternId : '' }`} /> @@ -163,6 +165,8 @@ export class ResultsLinks extends Component { } } +export const ResultsLinks = withKibana(ResultsLinksUI); + async function getFullTimeRange(index, timeFieldName) { const query = { bool: { must: [{ query_string: { analyze_wildcard: true, query: '*' } }] } }; const resp = await ml.getTimeFieldRange({ diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.js index 6ff0eb86f2c55..df9d9c1f9a3bc 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.js @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + import React from 'react'; import { @@ -22,14 +24,11 @@ import { FileContents } from '../file_contents'; import { AnalysisSummary } from '../analysis_summary'; import { FieldsStats } from '../fields_stats'; -export const ResultsView = injectI18n(function({ data, fileName, results, showEditFlyout, intl }) { - console.log(results); - +export const ResultsView = ({ data, fileName, results, showEditFlyout }) => { const tabs = [ { id: 'file-stats', - name: intl.formatMessage({ - id: 'xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName', + name: i18n.translate('xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName', { defaultMessage: 'File stats', }), content: , @@ -78,4 +77,4 @@ export const ResultsView = injectI18n(function({ data, fileName, results, showEd ); -}); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 149e3d1818e64..9dcb9d25692e9 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -5,9 +5,9 @@ */ import React, { FC, Fragment } from 'react'; -import { timefilter } from 'ui/timefilter'; +import { IUiSettingsClient } from 'src/core/public'; -import { KibanaConfigTypeFix } from '../../contexts/kibana'; +import { useMlKibana } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; import { getIndexPatternsContract } from '../../util/index_utils'; @@ -15,10 +15,12 @@ import { getIndexPatternsContract } from '../../util/index_utils'; import { FileDataVisualizerView } from './components/file_datavisualizer_view/index'; export interface FileDataVisualizerPageProps { - kibanaConfig: KibanaConfigTypeFix; + kibanaConfig: IUiSettingsClient; } export const FileDataVisualizerPage: FC = ({ kibanaConfig }) => { + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); const indexPatterns = getIndexPatternsContract(); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx index 9da1235a6becd..a2cc59bb38939 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx @@ -21,7 +21,7 @@ import { import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context'; +import { useUiSettings } from '../../../../../contexts/kibana/use_ui_settings_context'; export interface DocumentCountChartPoint { time: number | string; @@ -56,9 +56,7 @@ export const DocumentCountChart: FC = ({ const dateFormatter = niceTimeFormatter([timeRangeEarliest, timeRangeLatest]); - const IS_DARK_THEME = useUiChromeContext() - .getUiSettingsClient() - .get('theme:darkMode'); + const IS_DARK_THEME = useUiSettings().get('theme:darkMode'); const themeName = IS_DARK_THEME ? darkTheme : lightTheme; const EVENT_RATE_COLOR = themeName.euiColorVis2; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx index a7ad315dd968f..cf0e3ec1a9c9b 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx @@ -23,7 +23,7 @@ import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { MetricDistributionChartTooltipHeader } from './metric_distribution_chart_tooltip_header'; -import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context'; +import { useUiSettings } from '../../../../../contexts/kibana/use_ui_settings_context'; import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format'; import { ChartTooltipValue } from '../../../../../components/chart_tooltip/chart_tooltip_service'; @@ -52,9 +52,7 @@ export const MetricDistributionChart: FC = ({ width, height, chartData, f defaultMessage: 'distribution', }); - const IS_DARK_THEME = useUiChromeContext() - .getUiSettingsClient() - .get('theme:darkMode'); + const IS_DARK_THEME = useUiSettings().get('theme:darkMode'); const themeName = IS_DARK_THEME ? darkTheme : lightTheme; const AREA_SERIES_COLOR = themeName.euiColorVis1; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx index 5036a7d44aa8c..01ece9beddcea 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx @@ -23,8 +23,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; - +import { useMlKibana } from '../../../../contexts/kibana'; import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { FieldDataCard } from '../field_data_card'; import { FieldTypesSelect } from '../field_types_select'; @@ -62,13 +61,17 @@ export const FieldsPanel: FC = ({ setFieldSearchBarQuery, fieldVisConfigs, }) => { + const { + services: { notifications }, + } = useMlKibana(); function onShowAllFieldsChange() { setShowAllFields(!showAllFields); } function onSearchBarChange(query: SearchBarQuery) { if (query.error) { - toastNotifications.addWarning( + const { toasts } = notifications; + toasts.addWarning( i18n.translate('xpack.ml.datavisualizer.fieldsPanel.searchBarError', { defaultMessage: `An error occurred running the search. {message}.`, values: { message: query.error.message }, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx index 53125f00c590e..3306533d8e2ca 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search'; -import { SavedSearchQuery } from '../../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../../contexts/ml'; // @ts-ignore import { KqlFilterBar } from '../../../../components/kql_filter_bar/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts index 983908e2eb7f7..b0d8fa3d4fa88 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts @@ -6,10 +6,10 @@ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../util/dependency_cache'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; -import { SavedSearchQuery } from '../../../contexts/kibana'; +import { SavedSearchQuery } from '../../../contexts/ml'; import { IndexPatternTitle } from '../../../../../common/types/kibana'; import { ml } from '../../../services/ml_api_service'; @@ -92,6 +92,7 @@ export class DataLoader { } displayError(err: any) { + const toastNotifications = getToastNotifications(); if (err.statusCode === 500) { toastNotifications.addDanger( i18n.translate('xpack.ml.datavisualizer.dataLoader.internalServerErrorMessage', { diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 268cd86da74fd..8e99f2843ad1f 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -8,8 +8,6 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import { merge } from 'rxjs'; import { i18n } from '@kbn/i18n'; -import { timefilter } from 'ui/timefilter'; - import { EuiFlexGroup, EuiFlexItem, @@ -37,8 +35,9 @@ import { checkPermission } from '../../privilege/check_privilege'; import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; -import { useKibanaContext, SavedSearchQuery } from '../../contexts/kibana'; +import { useMlContext, SavedSearchQuery } from '../../contexts/ml'; import { kbnTypeToMLJobType } from '../../util/field_types_utils'; +import { useMlKibana } from '../../contexts/kibana'; import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils'; import { TimeBuckets } from '../../util/time_buckets'; import { useUrlState } from '../../util/url_state'; @@ -97,12 +96,13 @@ function getDefaultPageState(): DataVisualizerPageState { } export const Page: FC = () => { - const kibanaContext = useKibanaContext(); + const { services } = useMlKibana(); + const mlContext = useMlContext(); - const { combinedQuery, currentIndexPattern, currentSavedSearch, kibanaConfig } = kibanaContext; + const { timefilter } = services.data.query.timefilter; + const { combinedQuery, currentIndexPattern, currentSavedSearch, kibanaConfig } = mlContext; const dataLoader = new DataLoader(currentIndexPattern, kibanaConfig); - const [globalState, setGlobalState] = useUrlState('_g'); useEffect(() => { if (globalState?.time !== undefined) { diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index 819db630c0609..37794a250db34 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/legacy/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -61,8 +61,6 @@ const memoizedLoadTopInfluencers = memoize(loadTopInfluencers); const memoizedLoadViewBySwimlane = memoize(loadViewBySwimlane); const memoizedLoadAnomaliesTableData = memoize(loadAnomaliesTableData); -const dateFormatTz = getDateFormatTz(); - export interface LoadExplorerDataConfig { bounds: TimeRangeBounds; influencersFilterQuery: any; @@ -121,6 +119,8 @@ function loadExplorerData(config: LoadExplorerDataConfig): Observable ({ @@ -255,6 +254,7 @@ export class Explorer extends React.Component { } catch (e) { console.log('Invalid kuery syntax', e); // eslint-disable-line no-console + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable', { defaultMessage: @@ -351,6 +351,7 @@ export class Explorer extends React.Component { viewBySwimlaneData.laneLabels && viewBySwimlaneData.laneLabels.length > 0; + const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap index db9893a8a5c07..27b1278fa26db 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap @@ -28,7 +28,7 @@ exports[`ExplorerChartLabelBadge Render the chart label in one line. 1`] = ` d.anomalyScore !== undefined); - highlight = highlight && highlight.entity; + const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); - const filteredChartData = init(config); - drawRareChart(filteredChartData); + let vizWidth = 0; + const chartHeight = 170; + const LINE_CHART_ANOMALY_RADIUS = 7; + const SCHEDULED_EVENT_MARKER_HEIGHT = 5; - function init({ chartData }) { - const $el = $('.ml-explorer-chart'); + const chartType = getChartType(config); - // Clear any existing elements from the visualization, - // then build the svg elements for the chart. - const chartElement = d3.select(element).select('.content-wrapper'); - chartElement.select('svg').remove(); + // Left margin is adjusted later for longest y-axis label. + const margin = { top: 10, right: 0, bottom: 30, left: 0 }; + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { + margin.left = 60; + } - const svgWidth = $el.width(); - const svgHeight = chartHeight + margin.top + margin.bottom; + let lineChartXScale = null; + let lineChartYScale = null; + let lineChartGroup; + let lineChartValuesLine = null; + + const CHART_Y_ATTRIBUTE = chartType === CHART_TYPE.EVENT_DISTRIBUTION ? 'entity' : 'value'; + + let highlight = config.chartData.find(d => d.anomalyScore !== undefined); + highlight = highlight && highlight.entity; + + const filteredChartData = init(config); + drawRareChart(filteredChartData); + + function init({ chartData }) { + const $el = $('.ml-explorer-chart'); + + // Clear any existing elements from the visualization, + // then build the svg elements for the chart. + const chartElement = d3.select(element).select('.content-wrapper'); + chartElement.select('svg').remove(); + + const svgWidth = $el.width(); + const svgHeight = chartHeight + margin.top + margin.bottom; + + const svg = chartElement + .append('svg') + .classed('ml-explorer-chart-svg', true) + .attr('width', svgWidth) + .attr('height', svgHeight); + + const categoryLimit = 30; + const scaleCategories = d3 + .nest() + .key(d => d.entity) + .entries(chartData) + .sort((a, b) => { + return b.values.length - a.values.length; + }) + .filter((d, i) => { + // only filter for rare charts + if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { + return i < categoryLimit || d.key === highlight; + } + return true; + }) + .map(d => d.key); - const svg = chartElement - .append('svg') - .classed('ml-explorer-chart-svg', true) - .attr('width', svgWidth) - .attr('height', svgHeight); + chartData = chartData.filter(d => { + return scaleCategories.includes(d.entity); + }); - const categoryLimit = 30; - const scaleCategories = d3 - .nest() - .key(d => d.entity) - .entries(chartData) - .sort((a, b) => { - return b.values.length - a.values.length; - }) - .filter((d, i) => { - // only filter for rare charts - if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { - return i < categoryLimit || d.key === highlight; - } - return true; + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { + const focusData = chartData + .filter(d => { + return d.entity === highlight; }) - .map(d => d.key); + .map(d => d.value); + const focusExtent = d3.extent(focusData); + // now again filter chartData to include only the data points within the domain chartData = chartData.filter(d => { - return scaleCategories.includes(d.entity); + return d.value <= focusExtent[1]; }); - if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { - const focusData = chartData - .filter(d => { - return d.entity === highlight; - }) - .map(d => d.value); - const focusExtent = d3.extent(focusData); - - // now again filter chartData to include only the data points within the domain - chartData = chartData.filter(d => { - return d.value <= focusExtent[1]; - }); - - lineChartYScale = d3.scale - .linear() - .range([chartHeight, 0]) - .domain([0, focusExtent[1]]) - .nice(); - } else if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { - // avoid overflowing the border of the highlighted area - const rowMargin = 5; - lineChartYScale = d3.scale - .ordinal() - .rangePoints([rowMargin, chartHeight - rowMargin]) - .domain(scaleCategories); - } else { - throw `chartType '${chartType}' not supported`; - } + lineChartYScale = d3.scale + .linear() + .range([chartHeight, 0]) + .domain([0, focusExtent[1]]) + .nice(); + } else if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { + // avoid overflowing the border of the highlighted area + const rowMargin = 5; + lineChartYScale = d3.scale + .ordinal() + .rangePoints([rowMargin, chartHeight - rowMargin]) + .domain(scaleCategories); + } else { + throw `chartType '${chartType}' not supported`; + } - const yAxis = d3.svg - .axis() - .scale(lineChartYScale) - .orient('left') - .innerTickSize(0) - .outerTickSize(0) - .tickPadding(10); - - let maxYAxisLabelWidth = 0; - const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); - const tempLabelTextData = - chartType === CHART_TYPE.POPULATION_DISTRIBUTION - ? lineChartYScale.ticks() - : scaleCategories; - tempLabelText - .selectAll('text.temp.axis') - .data(tempLabelTextData) - .enter() - .append('text') - .text(d => { - if (fieldFormat !== undefined) { - return fieldFormat.convert(d, 'text'); - } else { - if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { - return lineChartYScale.tickFormat()(d); - } - return d; + const yAxis = d3.svg + .axis() + .scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); + + let maxYAxisLabelWidth = 0; + const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); + const tempLabelTextData = + chartType === CHART_TYPE.POPULATION_DISTRIBUTION + ? lineChartYScale.ticks() + : scaleCategories; + tempLabelText + .selectAll('text.temp.axis') + .data(tempLabelTextData) + .enter() + .append('text') + .text(d => { + if (fieldFormat !== undefined) { + return fieldFormat.convert(d, 'text'); + } else { + if (chartType === CHART_TYPE.POPULATION_DISTRIBUTION) { + return lineChartYScale.tickFormat()(d); } - }) - // Don't use an arrow function since we need access to `this`. - .each(function() { - maxYAxisLabelWidth = Math.max( - this.getBBox().width + yAxis.tickPadding(), - maxYAxisLabelWidth - ); - }) - .remove(); - d3.select('.temp-axis-label').remove(); - - // Set the size of the left margin according to the width of the largest y axis tick label - // if the chart is either a population chart or a rare chart below the cardinality threshold. - if ( - chartType === CHART_TYPE.POPULATION_DISTRIBUTION || - (chartType === CHART_TYPE.EVENT_DISTRIBUTION && - scaleCategories.length <= Y_AXIS_LABEL_THRESHOLD) - ) { - margin.left = Math.max(maxYAxisLabelWidth, 40); - } - vizWidth = svgWidth - margin.left - margin.right; - - // Set the x axis domain to match the request plot range. - // This ensures ranges on different charts will match, even when there aren't - // data points across the full range, and the selected anomalous region is centred. - lineChartXScale = d3.time - .scale() - .range([0, vizWidth]) - .domain([config.plotEarliest, config.plotLatest]); - - lineChartValuesLine = d3.svg - .line() - .x(d => lineChartXScale(d.date)) - .y(d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) - .defined(d => d.value !== null); - - lineChartGroup = svg - .append('g') - .attr('class', 'line-chart') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - return chartData; + return d; + } + }) + // Don't use an arrow function since we need access to `this`. + .each(function() { + maxYAxisLabelWidth = Math.max( + this.getBBox().width + yAxis.tickPadding(), + maxYAxisLabelWidth + ); + }) + .remove(); + d3.select('.temp-axis-label').remove(); + + // Set the size of the left margin according to the width of the largest y axis tick label + // if the chart is either a population chart or a rare chart below the cardinality threshold. + if ( + chartType === CHART_TYPE.POPULATION_DISTRIBUTION || + (chartType === CHART_TYPE.EVENT_DISTRIBUTION && + scaleCategories.length <= Y_AXIS_LABEL_THRESHOLD) + ) { + margin.left = Math.max(maxYAxisLabelWidth, 40); } + vizWidth = svgWidth - margin.left - margin.right; + + // Set the x axis domain to match the request plot range. + // This ensures ranges on different charts will match, even when there aren't + // data points across the full range, and the selected anomalous region is centred. + lineChartXScale = d3.time + .scale() + .range([0, vizWidth]) + .domain([config.plotEarliest, config.plotLatest]); + + lineChartValuesLine = d3.svg + .line() + .x(d => lineChartXScale(d.date)) + .y(d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) + .defined(d => d.value !== null); + + lineChartGroup = svg + .append('g') + .attr('class', 'line-chart') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + return chartData; + } + + function drawRareChart(data) { + // Add border round plot area. + lineChartGroup + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('height', chartHeight) + .attr('width', vizWidth) + .style('stroke', '#cccccc') + .style('fill', 'none') + .style('stroke-width', 1); + + drawRareChartAxes(); + drawRareChartHighlightedSpan(); + drawRareChartDots(data, lineChartGroup, lineChartValuesLine); + drawRareChartMarkers(data); + } + + function drawRareChartAxes() { + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('auto'); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + + const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); + // +1 ms to account for the ms that was subtracted for query aggregations. + const interval = config.selectedLatest - config.selectedEarliest + 1; + const tickValues = getTickValues( + tickValuesStart, + interval, + config.plotEarliest, + config.plotLatest + ); - function drawRareChart(data) { - // Add border round plot area. - lineChartGroup - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('height', chartHeight) - .attr('width', vizWidth) - .style('stroke', '#cccccc') - .style('fill', 'none') - .style('stroke-width', 1); - - drawRareChartAxes(); - drawRareChartHighlightedSpan(); - drawRareChartDots(data, lineChartGroup, lineChartValuesLine); - drawRareChartMarkers(data); + const xAxis = d3.svg + .axis() + .scale(lineChartXScale) + .orient('bottom') + .innerTickSize(-chartHeight) + .outerTickSize(0) + .tickPadding(10) + .tickFormat(d => moment(d).format(xAxisTickFormat)); + + // With tooManyBuckets the chart would end up with no x-axis labels + // because the ticks are based on the span of the emphasis section, + // and the highlighted area spans the whole chart. + if (tooManyBuckets === false) { + xAxis.tickValues(tickValues); + } else { + xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); } - function drawRareChartAxes() { - // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); - const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; - timeBuckets.setBounds(bounds); - timeBuckets.setInterval('auto'); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - - const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); - // +1 ms to account for the ms that was subtracted for query aggregations. - const interval = config.selectedLatest - config.selectedEarliest + 1; - const tickValues = getTickValues( - tickValuesStart, - interval, - config.plotEarliest, - config.plotLatest - ); - - const xAxis = d3.svg - .axis() - .scale(lineChartXScale) - .orient('bottom') - .innerTickSize(-chartHeight) - .outerTickSize(0) - .tickPadding(10) - .tickFormat(d => moment(d).format(xAxisTickFormat)); - - // With tooManyBuckets the chart would end up with no x-axis labels - // because the ticks are based on the span of the emphasis section, - // and the highlighted area spans the whole chart. - if (tooManyBuckets === false) { - xAxis.tickValues(tickValues); - } else { - xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); - } + const yAxis = d3.svg + .axis() + .scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); - const yAxis = d3.svg - .axis() - .scale(lineChartYScale) - .orient('left') - .innerTickSize(0) - .outerTickSize(0) - .tickPadding(10); + if (fieldFormat !== undefined) { + yAxis.tickFormat(d => fieldFormat.convert(d, 'text')); + } - if (fieldFormat !== undefined) { - yAxis.tickFormat(d => fieldFormat.convert(d, 'text')); - } + const axes = lineChartGroup.append('g'); - const axes = lineChartGroup.append('g'); + const gAxis = axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + chartHeight + ')') + .call(xAxis); - const gAxis = axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + chartHeight + ')') - .call(xAxis); + axes + .append('g') + .attr('class', 'y axis') + .call(yAxis); + // emphasize the y axis label this rare chart is actually about + if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { axes - .append('g') - .attr('class', 'y axis') - .call(yAxis); - - // emphasize the y axis label this rare chart is actually about - if (chartType === CHART_TYPE.EVENT_DISTRIBUTION) { - axes - .select('.y') - .selectAll('text') - .each(function(d) { - d3.select(this).classed('ml-explorer-chart-axis-emphasis', d === highlight); - }); - } + .select('.y') + .selectAll('text') + .each(function(d) { + d3.select(this).classed('ml-explorer-chart-axis-emphasis', d === highlight); + }); + } - if (tooManyBuckets === false) { - removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); - } + if (tooManyBuckets === false) { + removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); } + } - function drawRareChartDots(dotsData, rareChartGroup, rareChartValuesLine, radius = 1.5) { - // check if `g.values-dots` already exists, if not create it - // in both cases assign the element to `dotGroup` - const dotGroup = rareChartGroup.select('.values-dots').empty() - ? rareChartGroup.append('g').classed('values-dots', true) - : rareChartGroup.select('.values-dots'); - - // use d3's enter/update/exit pattern to render the dots - const dots = dotGroup.selectAll('circle').data(dotsData); - - dots - .enter() - .append('circle') - .classed('values-dots-circle', true) - .classed('values-dots-circle-blur', d => { - return d.entity !== highlight; - }) - .attr('r', d => (d.entity === highlight ? radius * 1.5 : radius)); + function drawRareChartDots(dotsData, rareChartGroup, rareChartValuesLine, radius = 1.5) { + // check if `g.values-dots` already exists, if not create it + // in both cases assign the element to `dotGroup` + const dotGroup = rareChartGroup.select('.values-dots').empty() + ? rareChartGroup.append('g').classed('values-dots', true) + : rareChartGroup.select('.values-dots'); - dots.attr('cx', rareChartValuesLine.x()).attr('cy', rareChartValuesLine.y()); + // use d3's enter/update/exit pattern to render the dots + const dots = dotGroup.selectAll('circle').data(dotsData); - dots.exit().remove(); - } + dots + .enter() + .append('circle') + .classed('values-dots-circle', true) + .classed('values-dots-circle-blur', d => { + return d.entity !== highlight; + }) + .attr('r', d => (d.entity === highlight ? radius * 1.5 : radius)); - function drawRareChartHighlightedSpan() { - // Draws a rectangle which highlights the time span that has been selected for view. - // Note depending on the overall time range and the bucket span, the selected time - // span may be longer than the range actually being plotted. - const rectStart = Math.max(config.selectedEarliest, config.plotEarliest); - const rectEnd = Math.min(config.selectedLatest, config.plotLatest); - const rectWidth = lineChartXScale(rectEnd) - lineChartXScale(rectStart); - - lineChartGroup - .append('rect') - .attr('class', 'selected-interval') - .attr('x', lineChartXScale(new Date(rectStart)) + 2) - .attr('y', 2) - .attr('rx', 3) - .attr('ry', 3) - .attr('width', rectWidth - 4) - .attr('height', chartHeight - 4); - } + dots.attr('cx', rareChartValuesLine.x()).attr('cy', rareChartValuesLine.y()); - function drawRareChartMarkers(data) { - // Render circle markers for the points. - // These are used for displaying tooltips on mouseover. - // Don't render dots where value=null (data gaps) - const dots = lineChartGroup - .append('g') - .attr('class', 'chart-markers') - .selectAll('.metric-value') - .data(data.filter(d => d.value !== null)); - - // Remove dots that are no longer needed i.e. if number of chart points has decreased. - dots.exit().remove(); - // Create any new dots that are needed i.e. if number of chart points has increased. - dots - .enter() - .append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - // Don't use an arrow function since we need access to `this`. - .on('mouseover', function(d) { - showLineChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Update all dots to new positions. - dots - .attr('cx', d => lineChartXScale(d.date)) - .attr('cy', d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) - .attr('class', d => { - let markerClass = 'metric-value'; - if (_.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity) { - markerClass += ' anomaly-marker '; - markerClass += getSeverityWithLow(d.anomalyScore).id; - } - return markerClass; - }); + dots.exit().remove(); + } - // Add rectangular markers for any scheduled events. - const scheduledEventMarkers = lineChartGroup - .select('.chart-markers') - .selectAll('.scheduled-event-marker') - .data(data.filter(d => d.scheduledEvents !== undefined)); - - // Remove markers that are no longer needed i.e. if number of chart points has decreased. - scheduledEventMarkers.exit().remove(); - // Create any new markers that are needed i.e. if number of chart points has increased. - scheduledEventMarkers - .enter() - .append('rect') - .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) - .attr('height', SCHEDULED_EVENT_MARKER_HEIGHT) - .attr('class', 'scheduled-event-marker') - .attr('rx', 1) - .attr('ry', 1); - - // Update all markers to new positions. - scheduledEventMarkers - .attr('x', d => lineChartXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) - .attr( - 'y', - d => lineChartYScale(d[CHART_Y_ATTRIBUTE]) - SCHEDULED_EVENT_MARKER_HEIGHT / 2 - ); - } + function drawRareChartHighlightedSpan() { + // Draws a rectangle which highlights the time span that has been selected for view. + // Note depending on the overall time range and the bucket span, the selected time + // span may be longer than the range actually being plotted. + const rectStart = Math.max(config.selectedEarliest, config.plotEarliest); + const rectEnd = Math.min(config.selectedLatest, config.plotLatest); + const rectWidth = lineChartXScale(rectEnd) - lineChartXScale(rectStart); + + lineChartGroup + .append('rect') + .attr('class', 'selected-interval') + .attr('x', lineChartXScale(new Date(rectStart)) + 2) + .attr('y', 2) + .attr('rx', 3) + .attr('ry', 3) + .attr('width', rectWidth - 4) + .attr('height', chartHeight - 4); + } - function showLineChartTooltip(marker, circle) { - // Show the time and metric values in the tooltip. - // Uses date, value, upper, lower and anomalyScore (optional) marker properties. - const formattedDate = formatHumanReadableDateTime(marker.date); - const tooltipData = [{ name: formattedDate }]; - const seriesKey = config.detectorLabel; + function drawRareChartMarkers(data) { + // Render circle markers for the points. + // These are used for displaying tooltips on mouseover. + // Don't render dots where value=null (data gaps) + const dots = lineChartGroup + .append('g') + .attr('class', 'chart-markers') + .selectAll('.metric-value') + .data(data.filter(d => d.value !== null)); + + // Remove dots that are no longer needed i.e. if number of chart points has decreased. + dots.exit().remove(); + // Create any new dots that are needed i.e. if number of chart points has increased. + dots + .enter() + .append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + // Don't use an arrow function since we need access to `this`. + .on('mouseover', function(d) { + showLineChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all dots to new positions. + dots + .attr('cx', d => lineChartXScale(d.date)) + .attr('cy', d => lineChartYScale(d[CHART_Y_ATTRIBUTE])) + .attr('class', d => { + let markerClass = 'metric-value'; + if (_.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity) { + markerClass += ' anomaly-marker '; + markerClass += getSeverityWithLow(d.anomalyScore).id; + } + return markerClass; + }); - if (_.has(marker, 'entity')) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.entityLabel', - defaultMessage: 'entity', - }), - value: marker.entity, - seriesKey, - }); - } + // Add rectangular markers for any scheduled events. + const scheduledEventMarkers = lineChartGroup + .select('.chart-markers') + .selectAll('.scheduled-event-marker') + .data(data.filter(d => d.scheduledEvents !== undefined)); + + // Remove markers that are no longer needed i.e. if number of chart points has decreased. + scheduledEventMarkers.exit().remove(); + // Create any new markers that are needed i.e. if number of chart points has increased. + scheduledEventMarkers + .enter() + .append('rect') + .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) + .attr('height', SCHEDULED_EVENT_MARKER_HEIGHT) + .attr('class', 'scheduled-event-marker') + .attr('rx', 1) + .attr('ry', 1); + + // Update all markers to new positions. + scheduledEventMarkers + .attr('x', d => lineChartXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) + .attr('y', d => lineChartYScale(d[CHART_Y_ATTRIBUTE]) - SCHEDULED_EVENT_MARKER_HEIGHT / 2); + } - if (_.has(marker, 'anomalyScore')) { - const score = parseInt(marker.anomalyScore); - const displayScore = score > 0 ? score : '< 1'; + function showLineChartTooltip(marker, circle) { + // Show the time and metric values in the tooltip. + // Uses date, value, upper, lower and anomalyScore (optional) marker properties. + const formattedDate = formatHumanReadableDateTime(marker.date); + const tooltipData = [{ name: formattedDate }]; + const seriesKey = config.detectorLabel; + + if (_.has(marker, 'entity')) { + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.distributionChart.entityLabel', { + defaultMessage: 'entity', + }), + value: marker.entity, + seriesKey, + }); + } + + if (_.has(marker, 'anomalyScore')) { + const score = parseInt(marker.anomalyScore); + const displayScore = score > 0 ? score : '< 1'; + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.distributionChart.anomalyScoreLabel', { + defaultMessage: 'anomaly score', + }), + value: displayScore, + color: getSeverityColor(score), + seriesKey, + yAccessor: 'anomaly_score', + }); + if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.anomalyScoreLabel', - defaultMessage: 'anomaly score', + name: i18n.translate('xpack.ml.explorer.distributionChart.valueLabel', { + defaultMessage: 'value', }), - value: displayScore, - color: getSeverityColor(score), + value: formatValue(marker.value, config.functionDescription, fieldFormat), seriesKey, - yAccessor: 'anomaly_score', + yAccessor: 'value', }); - if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { + if (typeof marker.numberOfCauses === 'undefined' || marker.numberOfCauses === 1) { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.valueLabel', - defaultMessage: 'value', + name: i18n.translate('xpack.ml.explorer.distributionChart.typicalLabel', { + defaultMessage: 'typical', }), - value: formatValue(marker.value, config.functionDescription, fieldFormat), + value: formatValue(marker.typical, config.functionDescription, fieldFormat), seriesKey, - yAccessor: 'value', + yAccessor: 'typical', }); - if (typeof marker.numberOfCauses === 'undefined' || marker.numberOfCauses === 1) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.typicalLabel', - defaultMessage: 'typical', - }), - value: formatValue(marker.typical, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'typical', - }); - } - if (typeof marker.byFieldName !== 'undefined' && _.has(marker, 'numberOfCauses')) { - tooltipData.push({ - name: intl.formatMessage( - { - id: 'xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel', - defaultMessage: - '{ numberOfCauses, plural, one {# unusual {byFieldName} value} other {#{plusSign} unusual {byFieldName} values}}', - }, - { + } + if (typeof marker.byFieldName !== 'undefined' && _.has(marker, 'numberOfCauses')) { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel', + { + defaultMessage: + '{ numberOfCauses, plural, one {# unusual {byFieldName} value} other {#{plusSign} unusual {byFieldName} values}}', + values: { numberOfCauses: marker.numberOfCauses, byFieldName: marker.byFieldName, // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. plusSign: marker.numberOfCauses < 10 ? '' : '+', - } - ), - seriesKey, - yAccessor: 'numberOfCauses', - }); - } - } - } else if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel', - defaultMessage: 'value', - }), - value: formatValue(marker.value, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'value', - }); - } - - if (_.has(marker, 'scheduledEvents')) { - marker.scheduledEvents.forEach((scheduledEvent, i) => { - tooltipData.push({ - name: intl.formatMessage( - { - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel', - defaultMessage: 'scheduled event{counter}', - }, - { counter: marker.scheduledEvents.length > 1 ? ` #${i + 1}` : '' } + }, + } ), - value: scheduledEvent, seriesKey, - yAccessor: `scheduled_events_${i + 1}`, + yAccessor: 'numberOfCauses', }); - }); + } } - - mlChartTooltipService.show(tooltipData, circle, { - x: LINE_CHART_ANOMALY_RADIUS * 3, - y: LINE_CHART_ANOMALY_RADIUS * 2, + } else if (chartType !== CHART_TYPE.EVENT_DISTRIBUTION) { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel', + { + defaultMessage: 'value', + } + ), + value: formatValue(marker.value, config.functionDescription, fieldFormat), + seriesKey, + yAccessor: 'value', }); } - } - shouldComponentUpdate() { - // Always return true, d3 will take care of appropriate re-rendering. - return true; - } + if (_.has(marker, 'scheduledEvents')) { + marker.scheduledEvents.forEach((scheduledEvent, i) => { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel', + { + defaultMessage: 'scheduled event{counter}', + values: { counter: marker.scheduledEvents.length > 1 ? ` #${i + 1}` : '' }, + } + ), + value: scheduledEvent, + seriesKey, + yAccessor: `scheduled_events_${i + 1}`, + }); + }); + } - setRef(componentNode) { - this.rootNode = componentNode; + mlChartTooltipService.show(tooltipData, circle, { + x: LINE_CHART_ANOMALY_RADIUS * 3, + y: LINE_CHART_ANOMALY_RADIUS * 2, + }); } + } - render() { - const { seriesConfig } = this.props; + shouldComponentUpdate() { + // Always return true, d3 will take care of appropriate re-rendering. + return true; + } - if (typeof seriesConfig === 'undefined') { - // just return so the empty directive renders without an error later on - return null; - } + setRef(componentNode) { + this.rootNode = componentNode; + } - // create a chart loading placeholder - const isLoading = seriesConfig.loading; + render() { + const { seriesConfig } = this.props; - return ( -

- {isLoading && } - {!isLoading &&
} -
- ); + if (typeof seriesConfig === 'undefined') { + // just return so the empty directive renders without an error later on + return null; } + + // create a chart loading placeholder + const isLoading = seriesConfig.loading; + + return ( +
+ {isLoading && } + {!isLoading &&
} +
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js index 313399b0260bc..71d777db5b2ec 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './explorer_chart_distribution.test.mocks'; import { chartData as mockChartData } from './__mocks__/mock_chart_data_rare'; import seriesConfig from './__mocks__/mock_series_config_rare.json'; @@ -22,12 +21,6 @@ jest.mock('../../services/field_format_service', () => ({ getFieldFormat: jest.fn(), }, })); -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - getUiSettingsClient: () => ({ - get: () => null, - }), -})); import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; @@ -51,9 +44,7 @@ describe('ExplorerChart', () => { test('Initialize', () => { const wrapper = mountWithIntl( - + ); // without setting any attributes and corresponding data @@ -69,7 +60,7 @@ describe('ExplorerChart', () => { }; const wrapper = mountWithIntl( - @@ -95,7 +86,7 @@ describe('ExplorerChart', () => { // We create the element including a wrapper which sets the width: return mountWithIntl(
- diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js index 5aab26f707252..5cf8245cd4739 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js @@ -10,7 +10,6 @@ import React from 'react'; import { CHART_TYPE } from '../explorer_constants'; import { i18n } from '@kbn/i18n'; -import { injectI18n } from '@kbn/i18n/react'; const CHART_DESCRIPTION = { [CHART_TYPE.EVENT_DISTRIBUTION]: i18n.translate( @@ -47,34 +46,30 @@ function TooltipDefinitionList({ toolTipData }) { ); } -export const ExplorerChartInfoTooltip = injectI18n(function ExplorerChartInfoTooltip({ +export const ExplorerChartInfoTooltip = ({ jobId, aggregationInterval, chartFunction, chartType, entityFields = [], - intl, -}) { +}) => { const chartDescription = CHART_DESCRIPTION[chartType]; const toolTipData = [ { - title: intl.formatMessage({ - id: 'xpack.ml.explorer.charts.infoTooltip.jobIdTitle', + title: i18n.translate('xpack.ml.explorer.charts.infoTooltip.jobIdTitle', { defaultMessage: 'job ID', }), description: jobId, }, { - title: intl.formatMessage({ - id: 'xpack.ml.explorer.charts.infoTooltip.aggregationIntervalTitle', + title: i18n.translate('xpack.ml.explorer.charts.infoTooltip.aggregationIntervalTitle', { defaultMessage: 'aggregation interval', }), description: aggregationInterval, }, { - title: intl.formatMessage({ - id: 'xpack.ml.explorer.charts.infoTooltip.chartFunctionTitle', + title: i18n.translate('xpack.ml.explorer.charts.infoTooltip.chartFunctionTitle', { defaultMessage: 'chart function', }), description: chartFunction, @@ -99,8 +94,8 @@ export const ExplorerChartInfoTooltip = injectI18n(function ExplorerChartInfoToo )}
); -}); -ExplorerChartInfoTooltip.WrappedComponent.propTypes = { +}; +ExplorerChartInfoTooltip.propTypes = { jobId: PropTypes.string.isRequired, aggregationInterval: PropTypes.string, chartFunction: PropTypes.string, diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js index 32b39131a9ae2..632c5a1006df5 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js @@ -23,7 +23,7 @@ describe('ExplorerChartTooltip', () => { jobId: 'mock-job-id', }; - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index a255b6b0434e4..d8d6709175090 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -43,490 +43,480 @@ import { mlEscape } from '../../util/string_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; const CONTENT_WRAPPER_HEIGHT = 215; const CONTENT_WRAPPER_CLASS = 'ml-explorer-chart-content-wrapper'; -export const ExplorerChartSingleMetric = injectI18n( - class ExplorerChartSingleMetric extends React.Component { - static propTypes = { - tooManyBuckets: PropTypes.bool, - seriesConfig: PropTypes.object, - severity: PropTypes.number.isRequired, - }; +export class ExplorerChartSingleMetric extends React.Component { + static propTypes = { + tooManyBuckets: PropTypes.bool, + seriesConfig: PropTypes.object, + severity: PropTypes.number.isRequired, + }; - componentDidMount() { - this.renderChart(); - } + componentDidMount() { + this.renderChart(); + } - componentDidUpdate() { - this.renderChart(); - } + componentDidUpdate() { + this.renderChart(); + } - renderChart() { - const { tooManyBuckets, intl } = this.props; + renderChart() { + const { tooManyBuckets } = this.props; - const element = this.rootNode; - const config = this.props.seriesConfig; - const severity = this.props.severity; + const element = this.rootNode; + const config = this.props.seriesConfig; + const severity = this.props.severity; - if (typeof config === 'undefined' || Array.isArray(config.chartData) === false) { - // just return so the empty directive renders without an error later on - return; - } + if (typeof config === 'undefined' || Array.isArray(config.chartData) === false) { + // just return so the empty directive renders without an error later on + return; + } - const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); - - let vizWidth = 0; - const chartHeight = 170; - - // Left margin is adjusted later for longest y-axis label. - const margin = { top: 10, right: 0, bottom: 30, left: 60 }; - - let lineChartXScale = null; - let lineChartYScale = null; - let lineChartGroup; - let lineChartValuesLine = null; - - init(config.chartLimits); - drawLineChart(config.chartData); - - function init(chartLimits) { - const $el = $('.ml-explorer-chart'); - - // Clear any existing elements from the visualization, - // then build the svg elements for the chart. - const chartElement = d3.select(element).select(`.${CONTENT_WRAPPER_CLASS}`); - chartElement.select('svg').remove(); - - const svgWidth = $el.width(); - const svgHeight = chartHeight + margin.top + margin.bottom; - - const svg = chartElement - .append('svg') - .classed('ml-explorer-chart-svg', true) - .attr('width', svgWidth) - .attr('height', svgHeight); - - // Set the size of the left margin according to the width of the largest y axis tick label. - lineChartYScale = d3.scale - .linear() - .range([chartHeight, 0]) - .domain([chartLimits.min, chartLimits.max]) - .nice(); - - const yAxis = d3.svg - .axis() - .scale(lineChartYScale) - .orient('left') - .innerTickSize(0) - .outerTickSize(0) - .tickPadding(10); - - let maxYAxisLabelWidth = 0; - const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); - tempLabelText - .selectAll('text.temp.axis') - .data(lineChartYScale.ticks()) - .enter() - .append('text') - .text(d => { - if (fieldFormat !== undefined) { - return fieldFormat.convert(d, 'text'); - } else { - return lineChartYScale.tickFormat()(d); - } - }) - // Don't use an arrow function since we need access to `this`. - .each(function() { - maxYAxisLabelWidth = Math.max( - this.getBBox().width + yAxis.tickPadding(), - maxYAxisLabelWidth - ); - }) - .remove(); - d3.select('.temp-axis-label').remove(); - - margin.left = Math.max(maxYAxisLabelWidth, 40); - vizWidth = svgWidth - margin.left - margin.right; - - // Set the x axis domain to match the request plot range. - // This ensures ranges on different charts will match, even when there aren't - // data points across the full range, and the selected anomalous region is centred. - lineChartXScale = d3.time - .scale() - .range([0, vizWidth]) - .domain([config.plotEarliest, config.plotLatest]); - - lineChartValuesLine = d3.svg - .line() - .x(d => lineChartXScale(d.date)) - .y(d => lineChartYScale(d.value)) - .defined(d => d.value !== null); - - lineChartGroup = svg - .append('g') - .attr('class', 'line-chart') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - } + const fieldFormat = mlFieldFormatService.getFieldFormat(config.jobId, config.detectorIndex); + + let vizWidth = 0; + const chartHeight = 170; + + // Left margin is adjusted later for longest y-axis label. + const margin = { top: 10, right: 0, bottom: 30, left: 60 }; + + let lineChartXScale = null; + let lineChartYScale = null; + let lineChartGroup; + let lineChartValuesLine = null; + + init(config.chartLimits); + drawLineChart(config.chartData); + + function init(chartLimits) { + const $el = $('.ml-explorer-chart'); + + // Clear any existing elements from the visualization, + // then build the svg elements for the chart. + const chartElement = d3.select(element).select(`.${CONTENT_WRAPPER_CLASS}`); + chartElement.select('svg').remove(); + + const svgWidth = $el.width(); + const svgHeight = chartHeight + margin.top + margin.bottom; + + const svg = chartElement + .append('svg') + .classed('ml-explorer-chart-svg', true) + .attr('width', svgWidth) + .attr('height', svgHeight); + + // Set the size of the left margin according to the width of the largest y axis tick label. + lineChartYScale = d3.scale + .linear() + .range([chartHeight, 0]) + .domain([chartLimits.min, chartLimits.max]) + .nice(); + + const yAxis = d3.svg + .axis() + .scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); + + let maxYAxisLabelWidth = 0; + const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); + tempLabelText + .selectAll('text.temp.axis') + .data(lineChartYScale.ticks()) + .enter() + .append('text') + .text(d => { + if (fieldFormat !== undefined) { + return fieldFormat.convert(d, 'text'); + } else { + return lineChartYScale.tickFormat()(d); + } + }) + // Don't use an arrow function since we need access to `this`. + .each(function() { + maxYAxisLabelWidth = Math.max( + this.getBBox().width + yAxis.tickPadding(), + maxYAxisLabelWidth + ); + }) + .remove(); + d3.select('.temp-axis-label').remove(); + + margin.left = Math.max(maxYAxisLabelWidth, 40); + vizWidth = svgWidth - margin.left - margin.right; + + // Set the x axis domain to match the request plot range. + // This ensures ranges on different charts will match, even when there aren't + // data points across the full range, and the selected anomalous region is centred. + lineChartXScale = d3.time + .scale() + .range([0, vizWidth]) + .domain([config.plotEarliest, config.plotLatest]); + + lineChartValuesLine = d3.svg + .line() + .x(d => lineChartXScale(d.date)) + .y(d => lineChartYScale(d.value)) + .defined(d => d.value !== null); + + lineChartGroup = svg + .append('g') + .attr('class', 'line-chart') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + } - function drawLineChart(data) { - // Add border round plot area. - lineChartGroup - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('height', chartHeight) - .attr('width', vizWidth) - .style('stroke', '#cccccc') - .style('fill', 'none') - .style('stroke-width', 1); - - drawLineChartAxes(); - drawLineChartHighlightedSpan(); - drawLineChartPaths(data); - drawLineChartDots(data, lineChartGroup, lineChartValuesLine); - drawLineChartMarkers(data); - } + function drawLineChart(data) { + // Add border round plot area. + lineChartGroup + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('height', chartHeight) + .attr('width', vizWidth) + .style('stroke', '#cccccc') + .style('fill', 'none') + .style('stroke-width', 1); + + drawLineChartAxes(); + drawLineChartHighlightedSpan(); + drawLineChartPaths(data); + drawLineChartDots(data, lineChartGroup, lineChartValuesLine); + drawLineChartMarkers(data); + } - function drawLineChartAxes() { - // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); - const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; - timeBuckets.setBounds(bounds); - timeBuckets.setInterval('auto'); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - - const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); - // +1 ms to account for the ms that was subtracted for query aggregations. - const interval = config.selectedLatest - config.selectedEarliest + 1; - const tickValues = getTickValues( - tickValuesStart, - interval, - config.plotEarliest, - config.plotLatest - ); + function drawLineChartAxes() { + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; + timeBuckets.setBounds(bounds); + timeBuckets.setInterval('auto'); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + + const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); + // +1 ms to account for the ms that was subtracted for query aggregations. + const interval = config.selectedLatest - config.selectedEarliest + 1; + const tickValues = getTickValues( + tickValuesStart, + interval, + config.plotEarliest, + config.plotLatest + ); - const xAxis = d3.svg - .axis() - .scale(lineChartXScale) - .orient('bottom') - .innerTickSize(-chartHeight) - .outerTickSize(0) - .tickPadding(10) - .tickFormat(d => moment(d).format(xAxisTickFormat)); - - // With tooManyBuckets the chart would end up with no x-axis labels - // because the ticks are based on the span of the emphasis section, - // and the highlighted area spans the whole chart. - if (tooManyBuckets === false) { - xAxis.tickValues(tickValues); - } else { - xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); - } + const xAxis = d3.svg + .axis() + .scale(lineChartXScale) + .orient('bottom') + .innerTickSize(-chartHeight) + .outerTickSize(0) + .tickPadding(10) + .tickFormat(d => moment(d).format(xAxisTickFormat)); + + // With tooManyBuckets the chart would end up with no x-axis labels + // because the ticks are based on the span of the emphasis section, + // and the highlighted area spans the whole chart. + if (tooManyBuckets === false) { + xAxis.tickValues(tickValues); + } else { + xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); + } - const yAxis = d3.svg - .axis() - .scale(lineChartYScale) - .orient('left') - .innerTickSize(0) - .outerTickSize(0) - .tickPadding(10); + const yAxis = d3.svg + .axis() + .scale(lineChartYScale) + .orient('left') + .innerTickSize(0) + .outerTickSize(0) + .tickPadding(10); - if (fieldFormat !== undefined) { - yAxis.tickFormat(d => fieldFormat.convert(d, 'text')); - } + if (fieldFormat !== undefined) { + yAxis.tickFormat(d => fieldFormat.convert(d, 'text')); + } - const axes = lineChartGroup.append('g'); + const axes = lineChartGroup.append('g'); - const gAxis = axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + chartHeight + ')') - .call(xAxis); + const gAxis = axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + chartHeight + ')') + .call(xAxis); - axes - .append('g') - .attr('class', 'y axis') - .call(yAxis); + axes + .append('g') + .attr('class', 'y axis') + .call(yAxis); - if (tooManyBuckets === false) { - removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); - } + if (tooManyBuckets === false) { + removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); } + } - function drawLineChartHighlightedSpan() { - // Draws a rectangle which highlights the time span that has been selected for view. - // Note depending on the overall time range and the bucket span, the selected time - // span may be longer than the range actually being plotted. - const rectStart = Math.max(config.selectedEarliest, config.plotEarliest); - const rectEnd = Math.min(config.selectedLatest, config.plotLatest); - const rectWidth = lineChartXScale(rectEnd) - lineChartXScale(rectStart); - - lineChartGroup - .append('rect') - .attr('class', 'selected-interval') - .attr('x', lineChartXScale(new Date(rectStart)) + 2) - .attr('y', 2) - .attr('rx', 3) - .attr('ry', 3) - .attr('width', rectWidth - 4) - .attr('height', chartHeight - 4); - } + function drawLineChartHighlightedSpan() { + // Draws a rectangle which highlights the time span that has been selected for view. + // Note depending on the overall time range and the bucket span, the selected time + // span may be longer than the range actually being plotted. + const rectStart = Math.max(config.selectedEarliest, config.plotEarliest); + const rectEnd = Math.min(config.selectedLatest, config.plotLatest); + const rectWidth = lineChartXScale(rectEnd) - lineChartXScale(rectStart); + + lineChartGroup + .append('rect') + .attr('class', 'selected-interval') + .attr('x', lineChartXScale(new Date(rectStart)) + 2) + .attr('y', 2) + .attr('rx', 3) + .attr('ry', 3) + .attr('width', rectWidth - 4) + .attr('height', chartHeight - 4); + } - function drawLineChartPaths(data) { - lineChartGroup - .append('path') - .attr('class', 'values-line') - .attr('d', lineChartValuesLine(data)); - } + function drawLineChartPaths(data) { + lineChartGroup + .append('path') + .attr('class', 'values-line') + .attr('d', lineChartValuesLine(data)); + } - function drawLineChartMarkers(data) { - // Render circle markers for the points. - // These are used for displaying tooltips on mouseover. - // Don't render dots where value=null (data gaps, with no anomalies) - // or for multi-bucket anomalies. - const dots = lineChartGroup - .append('g') - .attr('class', 'chart-markers') - .selectAll('.metric-value') - .data( - data.filter( - d => - (d.value !== null || typeof d.anomalyScore === 'number') && - !showMultiBucketAnomalyMarker(d) - ) - ); + function drawLineChartMarkers(data) { + // Render circle markers for the points. + // These are used for displaying tooltips on mouseover. + // Don't render dots where value=null (data gaps, with no anomalies) + // or for multi-bucket anomalies. + const dots = lineChartGroup + .append('g') + .attr('class', 'chart-markers') + .selectAll('.metric-value') + .data( + data.filter( + d => + (d.value !== null || typeof d.anomalyScore === 'number') && + !showMultiBucketAnomalyMarker(d) + ) + ); - // Remove dots that are no longer needed i.e. if number of chart points has decreased. - dots.exit().remove(); - // Create any new dots that are needed i.e. if number of chart points has increased. - dots - .enter() - .append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - // Don't use an arrow function since we need access to `this`. - .on('mouseover', function(d) { - showLineChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - const isAnomalyVisible = d => - _.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity; - - // Update all dots to new positions. - dots - .attr('cx', d => lineChartXScale(d.date)) - .attr('cy', d => lineChartYScale(d.value)) - .attr('class', d => { - let markerClass = 'metric-value'; - if (isAnomalyVisible(d)) { - markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`; - } - return markerClass; - }); + // Remove dots that are no longer needed i.e. if number of chart points has decreased. + dots.exit().remove(); + // Create any new dots that are needed i.e. if number of chart points has increased. + dots + .enter() + .append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + // Don't use an arrow function since we need access to `this`. + .on('mouseover', function(d) { + showLineChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + const isAnomalyVisible = d => _.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity; + + // Update all dots to new positions. + dots + .attr('cx', d => lineChartXScale(d.date)) + .attr('cy', d => lineChartYScale(d.value)) + .attr('class', d => { + let markerClass = 'metric-value'; + if (isAnomalyVisible(d)) { + markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`; + } + return markerClass; + }); - // Render cross symbols for any multi-bucket anomalies. - const multiBucketMarkers = lineChartGroup - .select('.chart-markers') - .selectAll('.multi-bucket') - .data(data.filter(d => isAnomalyVisible(d) && showMultiBucketAnomalyMarker(d) === true)); - - // Remove multi-bucket markers that are no longer needed - multiBucketMarkers.exit().remove(); - - // Append the multi-bucket markers and position on chart. - multiBucketMarkers - .enter() - .append('path') - .attr( - 'd', - d3.svg - .symbol() - .size(MULTI_BUCKET_SYMBOL_SIZE) - .type('cross') - ) - .attr( - 'transform', - d => `translate(${lineChartXScale(d.date)}, ${lineChartYScale(d.value)})` - ) - .attr( - 'class', - d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}` - ) - // Don't use an arrow function since we need access to `this`. - .on('mouseover', function(d) { - showLineChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Add rectangular markers for any scheduled events. - const scheduledEventMarkers = lineChartGroup - .select('.chart-markers') - .selectAll('.scheduled-event-marker') - .data(data.filter(d => d.scheduledEvents !== undefined)); - - // Remove markers that are no longer needed i.e. if number of chart points has decreased. - scheduledEventMarkers.exit().remove(); - // Create any new markers that are needed i.e. if number of chart points has increased. - scheduledEventMarkers - .enter() - .append('rect') - .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) - .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) - .attr('class', 'scheduled-event-marker') - .attr('rx', 1) - .attr('ry', 1); - - // Update all markers to new positions. - scheduledEventMarkers - .attr('x', d => lineChartXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) - .attr('y', d => lineChartYScale(d.value) - SCHEDULED_EVENT_SYMBOL_HEIGHT / 2); - } + // Render cross symbols for any multi-bucket anomalies. + const multiBucketMarkers = lineChartGroup + .select('.chart-markers') + .selectAll('.multi-bucket') + .data(data.filter(d => isAnomalyVisible(d) && showMultiBucketAnomalyMarker(d) === true)); + + // Remove multi-bucket markers that are no longer needed + multiBucketMarkers.exit().remove(); + + // Append the multi-bucket markers and position on chart. + multiBucketMarkers + .enter() + .append('path') + .attr( + 'd', + d3.svg + .symbol() + .size(MULTI_BUCKET_SYMBOL_SIZE) + .type('cross') + ) + .attr( + 'transform', + d => `translate(${lineChartXScale(d.date)}, ${lineChartYScale(d.value)})` + ) + .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}`) + // Don't use an arrow function since we need access to `this`. + .on('mouseover', function(d) { + showLineChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Add rectangular markers for any scheduled events. + const scheduledEventMarkers = lineChartGroup + .select('.chart-markers') + .selectAll('.scheduled-event-marker') + .data(data.filter(d => d.scheduledEvents !== undefined)); + + // Remove markers that are no longer needed i.e. if number of chart points has decreased. + scheduledEventMarkers.exit().remove(); + // Create any new markers that are needed i.e. if number of chart points has increased. + scheduledEventMarkers + .enter() + .append('rect') + .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) + .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) + .attr('class', 'scheduled-event-marker') + .attr('rx', 1) + .attr('ry', 1); + + // Update all markers to new positions. + scheduledEventMarkers + .attr('x', d => lineChartXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) + .attr('y', d => lineChartYScale(d.value) - SCHEDULED_EVENT_SYMBOL_HEIGHT / 2); + } - function showLineChartTooltip(marker, circle) { - // Show the time and metric values in the tooltip. - // Uses date, value, upper, lower and anomalyScore (optional) marker properties. - const formattedDate = formatHumanReadableDateTime(marker.date); - const tooltipData = [{ name: formattedDate }]; - const seriesKey = config.detectorLabel; + function showLineChartTooltip(marker, circle) { + // Show the time and metric values in the tooltip. + // Uses date, value, upper, lower and anomalyScore (optional) marker properties. + const formattedDate = formatHumanReadableDateTime(marker.date); + const tooltipData = [{ name: formattedDate }]; + const seriesKey = config.detectorLabel; + + if (_.has(marker, 'anomalyScore')) { + const score = parseInt(marker.anomalyScore); + const displayScore = score > 0 ? score : '< 1'; + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.singleMetricChart.anomalyScoreLabel', { + defaultMessage: 'anomaly score', + }), + value: displayScore, + color: getSeverityColor(score), + seriesKey, + yAccessor: 'anomaly_score', + }); - if (_.has(marker, 'anomalyScore')) { - const score = parseInt(marker.anomalyScore); - const displayScore = score > 0 ? score : '< 1'; + if (showMultiBucketAnomalyTooltip(marker) === true) { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.anomalyScoreLabel', - defaultMessage: 'anomaly score', + name: i18n.translate('xpack.ml.explorer.singleMetricChart.multiBucketImpactLabel', { + defaultMessage: 'multi-bucket impact', }), - value: displayScore, - color: getSeverityColor(score), + value: getMultiBucketImpactLabel(marker.multiBucketImpact), seriesKey, - yAccessor: 'anomaly_score', + yAccessor: 'multi_bucket_impact', }); + } - if (showMultiBucketAnomalyTooltip(marker) === true) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.multiBucketImpactLabel', - defaultMessage: 'multi-bucket impact', - }), - value: getMultiBucketImpactLabel(marker.multiBucketImpact), - seriesKey, - yAccessor: 'multi_bucket_impact', - }); - } - - // Show actual/typical when available except for rare detectors. - // Rare detectors always have 1 as actual and the probability as typical. - // Exposing those values in the tooltip with actual/typical labels might irritate users. - if (_.has(marker, 'actual') && config.functionDescription !== 'rare') { - // Display the record actual in preference to the chart value, which may be - // different depending on the aggregation interval of the chart. - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.actualLabel', - defaultMessage: 'actual', - }), - value: formatValue(marker.actual, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'actual', - }); - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.typicalLabel', - defaultMessage: 'typical', - }), - value: formatValue(marker.typical, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'typical', - }); - } else { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.valueLabel', - defaultMessage: 'value', - }), - value: formatValue(marker.value, config.functionDescription, fieldFormat), - seriesKey, - yAccessor: 'value', - }); - if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { - tooltipData.push({ - name: intl.formatMessage( - { - id: 'xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel', - defaultMessage: - '{ numberOfCauses, plural, one {# unusual {byFieldName} value} other {#{plusSign} unusual {byFieldName} values}}', - }, - { - numberOfCauses: marker.numberOfCauses, - byFieldName: marker.byFieldName, - // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. - plusSign: marker.numberOfCauses < 10 ? '' : '+', - } - ), - seriesKey, - yAccessor: 'numberOfCauses', - }); - } - } + // Show actual/typical when available except for rare detectors. + // Rare detectors always have 1 as actual and the probability as typical. + // Exposing those values in the tooltip with actual/typical labels might irritate users. + if (_.has(marker, 'actual') && config.functionDescription !== 'rare') { + // Display the record actual in preference to the chart value, which may be + // different depending on the aggregation interval of the chart. + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.singleMetricChart.actualLabel', { + defaultMessage: 'actual', + }), + value: formatValue(marker.actual, config.functionDescription, fieldFormat), + seriesKey, + yAccessor: 'actual', + }); + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.singleMetricChart.typicalLabel', { + defaultMessage: 'typical', + }), + value: formatValue(marker.typical, config.functionDescription, fieldFormat), + seriesKey, + yAccessor: 'typical', + }); } else { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.valueWithoutAnomalyScoreLabel', + name: i18n.translate('xpack.ml.explorer.singleMetricChart.valueLabel', { defaultMessage: 'value', }), value: formatValue(marker.value, config.functionDescription, fieldFormat), seriesKey, yAccessor: 'value', }); + if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.explorer.distributionChart.unusualByFieldValuesLabel', + { + defaultMessage: + '{ numberOfCauses, plural, one {# unusual {byFieldName} value} other {#{plusSign} unusual {byFieldName} values}}', + values: { + numberOfCauses: marker.numberOfCauses, + byFieldName: marker.byFieldName, + // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. + plusSign: marker.numberOfCauses < 10 ? '' : '+', + }, + } + ), + seriesKey, + yAccessor: 'numberOfCauses', + }); + } } - - if (_.has(marker, 'scheduledEvents')) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.singleMetricChart.scheduledEventsLabel', - defaultMessage: 'Scheduled events', - }), - value: marker.scheduledEvents.map(mlEscape).join('
'), - }); - } - - mlChartTooltipService.show(tooltipData, circle, { - x: LINE_CHART_ANOMALY_RADIUS * 3, - y: LINE_CHART_ANOMALY_RADIUS * 2, + } else { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.explorer.singleMetricChart.valueWithoutAnomalyScoreLabel', + { + defaultMessage: 'value', + } + ), + value: formatValue(marker.value, config.functionDescription, fieldFormat), + seriesKey, + yAccessor: 'value', }); } - } - shouldComponentUpdate() { - // Always return true, d3 will take care of appropriate re-rendering. - return true; - } + if (_.has(marker, 'scheduledEvents')) { + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.singleMetricChart.scheduledEventsLabel', { + defaultMessage: 'Scheduled events', + }), + value: marker.scheduledEvents.map(mlEscape).join('
'), + }); + } - setRef(componentNode) { - this.rootNode = componentNode; + mlChartTooltipService.show(tooltipData, circle, { + x: LINE_CHART_ANOMALY_RADIUS * 3, + y: LINE_CHART_ANOMALY_RADIUS * 2, + }); } + } - render() { - const { seriesConfig } = this.props; + shouldComponentUpdate() { + // Always return true, d3 will take care of appropriate re-rendering. + return true; + } - if (typeof seriesConfig === 'undefined') { - // just return so the empty directive renders without an error later on - return null; - } + setRef(componentNode) { + this.rootNode = componentNode; + } - // create a chart loading placeholder - const isLoading = seriesConfig.loading; + render() { + const { seriesConfig } = this.props; - return ( -
- {isLoading && } - {!isLoading &&
} -
- ); + if (typeof seriesConfig === 'undefined') { + // just return so the empty directive renders without an error later on + return null; } + + // create a chart loading placeholder + const isLoading = seriesConfig.loading; + + return ( +
+ {isLoading && } + {!isLoading &&
} +
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js index d291dbb23d016..ca3e52308a936 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './explorer_chart_single_metric.test.mocks'; import { chartData as mockChartData } from './__mocks__/mock_chart_data'; import seriesConfig from './__mocks__/mock_series_config_filebeat.json'; @@ -22,12 +21,6 @@ jest.mock('../../services/field_format_service', () => ({ getFieldFormat: jest.fn(), }, })); -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - getUiSettingsClient: () => ({ - get: () => null, - }), -})); import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; @@ -51,9 +44,7 @@ describe('ExplorerChart', () => { test('Initialize', () => { const wrapper = mountWithIntl( - + ); // without setting any attributes and corresponding data @@ -69,7 +60,7 @@ describe('ExplorerChart', () => { }; const wrapper = mountWithIntl( - @@ -95,7 +86,7 @@ describe('ExplorerChart', () => { // We create the element including a wrapper which sets the width: return mountWithIntl(
- diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts deleted file mode 100644 index 46178a7d02977..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('ui/timefilter', () => { - return {}; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js index 4b2d307e72c66..3a6c8c8790def 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js @@ -14,7 +14,6 @@ import { chartLimits } from '../../util/chart_utils'; import { getDefaultChartsData } from './explorer_charts_container_service'; import { ExplorerChartsContainer } from './explorer_charts_container'; -import './explorer_chart_single_metric.test.mocks'; import { chartData } from './__mocks__/mock_chart_data'; import seriesConfig from './__mocks__/mock_series_config_filebeat.json'; import seriesConfigRare from './__mocks__/mock_series_config_rare.json'; @@ -39,22 +38,6 @@ jest.mock('../../services/job_service', () => ({ }, })); -// The mocks for ui/chrome and ui/timefilter are copied from charts_utils.test.js -// TODO: Refactor the involved tests to avoid this duplication -jest.mock( - 'ui/chrome', - () => ({ - addBasePath: () => '/api/ml', - getBasePath: () => { - return ''; - }, - getInjected: () => true, - }), - { virtual: true } -); - -jest.mock('ui/new_platform'); - describe('ExplorerChartsContainer', () => { const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 }; const originalGetBBox = SVGElement.prototype.getBBox; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.mocks.ts deleted file mode 100644 index 46178a7d02977..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.mocks.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('ui/timefilter', () => { - return {}; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js index fbbf5eb324095..35261257ce625 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './explorer_charts_container_service.test.mocks'; import _ from 'lodash'; import mockAnomalyChartRecords from './__mocks__/mock_anomaly_chart_records.json'; @@ -95,13 +94,6 @@ jest.mock('../legacy_utils', () => ({ }, })); -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - getUiSettingsClient: () => ({ - get: () => null, - }), -})); - jest.mock('../explorer_dashboard_service', () => ({ explorerService: { setCharts: jest.fn(), diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts deleted file mode 100644 index 46178a7d02977..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('ui/timefilter', () => { - return {}; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js index 7ae9d215d7034..6582f5c609864 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js @@ -24,7 +24,6 @@ import { mlEscape } from '../util/string_utils'; import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip_service'; import { ALLOW_CELL_RANGE_SELECTION, dragSelect$ } from './explorer_dashboard_service'; import { DRAG_SELECT_ACTION } from './explorer_constants'; -import { injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; const SCSS = { @@ -32,581 +31,574 @@ const SCSS = { mlHideRangeSelection: 'mlHideRangeSelection', }; -export const ExplorerSwimlane = injectI18n( - class ExplorerSwimlane extends React.Component { - static propTypes = { - chartWidth: PropTypes.number.isRequired, - filterActive: PropTypes.bool, - maskAll: PropTypes.bool, - TimeBuckets: PropTypes.func.isRequired, - swimlaneCellClick: PropTypes.func.isRequired, - swimlaneData: PropTypes.shape({ - laneLabels: PropTypes.array.isRequired, - }).isRequired, - swimlaneType: PropTypes.string.isRequired, - selection: PropTypes.object, - swimlaneRenderDoneListener: PropTypes.func.isRequired, - }; +export class ExplorerSwimlane extends React.Component { + static propTypes = { + chartWidth: PropTypes.number.isRequired, + filterActive: PropTypes.bool, + maskAll: PropTypes.bool, + TimeBuckets: PropTypes.func.isRequired, + swimlaneCellClick: PropTypes.func.isRequired, + swimlaneData: PropTypes.shape({ + laneLabels: PropTypes.array.isRequired, + }).isRequired, + swimlaneType: PropTypes.string.isRequired, + selection: PropTypes.object, + swimlaneRenderDoneListener: PropTypes.func.isRequired, + }; + + // Since this component is mostly rendered using d3 and cellMouseoverActive is only + // relevant for d3 based interaction, we don't manage this using React's state + // and intentionally circumvent the component lifecycle when updating it. + cellMouseoverActive = true; + + dragSelectSubscriber = null; + + componentDidMount() { + // property for data comparison to be able to filter + // consecutive click events with the same data. + let previousSelectedData = null; + + // Listen for dragSelect events + this.dragSelectSubscriber = dragSelect$.subscribe(({ action, elements = [] }) => { + const element = d3.select(this.rootNode.parentNode); + const { swimlaneType } = this.props; - // Since this component is mostly rendered using d3 and cellMouseoverActive is only - // relevant for d3 based interaction, we don't manage this using React's state - // and intentionally circumvent the component lifecycle when updating it. - cellMouseoverActive = true; - - dragSelectSubscriber = null; - - componentDidMount() { - // property for data comparison to be able to filter - // consecutive click events with the same data. - let previousSelectedData = null; - - // Listen for dragSelect events - this.dragSelectSubscriber = dragSelect$.subscribe(({ action, elements = [] }) => { - const element = d3.select(this.rootNode.parentNode); - const { swimlaneType } = this.props; - - if (action === DRAG_SELECT_ACTION.NEW_SELECTION && elements.length > 0) { - element.classed(SCSS.mlDragselectDragging, false); - const firstSelectedCell = d3.select(elements[0]).node().__clickData__; - - if ( - typeof firstSelectedCell !== 'undefined' && - swimlaneType === firstSelectedCell.swimlaneType - ) { - const selectedData = elements.reduce( - (d, e) => { - const cell = d3.select(e).node().__clickData__; - d.bucketScore = Math.max(d.bucketScore, cell.bucketScore); - d.laneLabels.push(cell.laneLabel); - d.times.push(cell.time); - return d; - }, - { - bucketScore: 0, - laneLabels: [], - times: [], - } - ); - - selectedData.laneLabels = _.uniq(selectedData.laneLabels); - selectedData.times = _.uniq(selectedData.times); - if (_.isEqual(selectedData, previousSelectedData) === false) { - // If no cells containing anomalies have been selected, - // immediately clear the selection, otherwise trigger - // a reload with the updated selected cells. - if (selectedData.bucketScore === 0) { - elements.map(e => d3.select(e).classed('ds-selected', false)); - this.selectCell([], selectedData); - previousSelectedData = null; - } else { - this.selectCell(elements, selectedData); - previousSelectedData = selectedData; - } + if (action === DRAG_SELECT_ACTION.NEW_SELECTION && elements.length > 0) { + element.classed(SCSS.mlDragselectDragging, false); + const firstSelectedCell = d3.select(elements[0]).node().__clickData__; + + if ( + typeof firstSelectedCell !== 'undefined' && + swimlaneType === firstSelectedCell.swimlaneType + ) { + const selectedData = elements.reduce( + (d, e) => { + const cell = d3.select(e).node().__clickData__; + d.bucketScore = Math.max(d.bucketScore, cell.bucketScore); + d.laneLabels.push(cell.laneLabel); + d.times.push(cell.time); + return d; + }, + { + bucketScore: 0, + laneLabels: [], + times: [], } - } + ); - this.cellMouseoverActive = true; - } else if (action === DRAG_SELECT_ACTION.ELEMENT_SELECT) { - element.classed(SCSS.mlDragselectDragging, true); - } else if (action === DRAG_SELECT_ACTION.DRAG_START) { - previousSelectedData = null; - this.cellMouseoverActive = false; - mlChartTooltipService.hide(true); + selectedData.laneLabels = _.uniq(selectedData.laneLabels); + selectedData.times = _.uniq(selectedData.times); + if (_.isEqual(selectedData, previousSelectedData) === false) { + // If no cells containing anomalies have been selected, + // immediately clear the selection, otherwise trigger + // a reload with the updated selected cells. + if (selectedData.bucketScore === 0) { + elements.map(e => d3.select(e).classed('ds-selected', false)); + this.selectCell([], selectedData); + previousSelectedData = null; + } else { + this.selectCell(elements, selectedData); + previousSelectedData = selectedData; + } + } } - }); - - this.renderSwimlane(); - } - - componentDidUpdate() { - this.renderSwimlane(); - } - componentWillUnmount() { - if (this.dragSelectSubscriber !== null) { - this.dragSelectSubscriber.unsubscribe(); + this.cellMouseoverActive = true; + } else if (action === DRAG_SELECT_ACTION.ELEMENT_SELECT) { + element.classed(SCSS.mlDragselectDragging, true); + } else if (action === DRAG_SELECT_ACTION.DRAG_START) { + previousSelectedData = null; + this.cellMouseoverActive = false; + mlChartTooltipService.hide(true); } - const element = d3.select(this.rootNode); - element.html(''); - } + }); - selectCell(cellsToSelect, { laneLabels, bucketScore, times }) { - const { selection, swimlaneCellClick, swimlaneData, swimlaneType } = this.props; + this.renderSwimlane(); + } - let triggerNewSelection = false; + componentDidUpdate() { + this.renderSwimlane(); + } - if (cellsToSelect.length > 1 || bucketScore > 0) { - triggerNewSelection = true; - } + componentWillUnmount() { + if (this.dragSelectSubscriber !== null) { + this.dragSelectSubscriber.unsubscribe(); + } + const element = d3.select(this.rootNode); + element.html(''); + } - // Check if the same cells were selected again, if so clear the selection, - // otherwise activate the new selection. The two objects are built for - // comparison because we cannot simply compare to "appState.mlExplorerSwimlane" - // since it also includes the "viewBy" attribute which might differ depending - // on whether the overall or viewby swimlane was selected. - const oldSelection = { - selectedType: selection && selection.type, - selectedLanes: selection && selection.lanes, - selectedTimes: selection && selection.times, - }; + selectCell(cellsToSelect, { laneLabels, bucketScore, times }) { + const { selection, swimlaneCellClick, swimlaneData, swimlaneType } = this.props; - const newSelection = { - selectedType: swimlaneType, - selectedLanes: laneLabels, - selectedTimes: d3.extent(times), - }; + let triggerNewSelection = false; - if (_.isEqual(oldSelection, newSelection)) { - triggerNewSelection = false; - } + if (cellsToSelect.length > 1 || bucketScore > 0) { + triggerNewSelection = true; + } - if (triggerNewSelection === false) { - swimlaneCellClick({}); - return; - } + // Check if the same cells were selected again, if so clear the selection, + // otherwise activate the new selection. The two objects are built for + // comparison because we cannot simply compare to "appState.mlExplorerSwimlane" + // since it also includes the "viewBy" attribute which might differ depending + // on whether the overall or viewby swimlane was selected. + const oldSelection = { + selectedType: selection && selection.type, + selectedLanes: selection && selection.lanes, + selectedTimes: selection && selection.times, + }; - const selectedCells = { - viewByFieldName: swimlaneData.fieldName, - lanes: laneLabels, - times: d3.extent(times), - type: swimlaneType, - }; - swimlaneCellClick(selectedCells); + const newSelection = { + selectedType: swimlaneType, + selectedLanes: laneLabels, + selectedTimes: d3.extent(times), + }; + + if (_.isEqual(oldSelection, newSelection)) { + triggerNewSelection = false; } - highlightOverall(times) { - const overallSwimlane = d3.select('.ml-swimlane-overall'); - times.forEach(time => { - const overallCell = overallSwimlane - .selectAll(`div[data-time="${time}"]`) - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect'); - overallCell.classed('sl-cell-inner-selected', true); - }); + if (triggerNewSelection === false) { + swimlaneCellClick({}); + return; } - highlightSelection(cellsToSelect, laneLabels, times) { - const { swimlaneType } = this.props; + const selectedCells = { + viewByFieldName: swimlaneData.fieldName, + lanes: laneLabels, + times: d3.extent(times), + type: swimlaneType, + }; + swimlaneCellClick(selectedCells); + } - // This selects both overall and viewby swimlane - const wrapper = d3.selectAll('.ml-explorer-swimlane'); + highlightOverall(times) { + const overallSwimlane = d3.select('.ml-swimlane-overall'); + times.forEach(time => { + const overallCell = overallSwimlane + .selectAll(`div[data-time="${time}"]`) + .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect'); + overallCell.classed('sl-cell-inner-selected', true); + }); + } - wrapper.selectAll('.lane-label').classed('lane-label-masked', true); - wrapper + highlightSelection(cellsToSelect, laneLabels, times) { + const { swimlaneType } = this.props; + + // This selects both overall and viewby swimlane + const wrapper = d3.selectAll('.ml-explorer-swimlane'); + + wrapper.selectAll('.lane-label').classed('lane-label-masked', true); + wrapper + .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') + .classed('sl-cell-inner-masked', true); + wrapper + .selectAll( + '.sl-cell-inner.sl-cell-inner-selected,.sl-cell-inner-dragselect.sl-cell-inner-selected' + ) + .classed('sl-cell-inner-selected', false); + + d3.selectAll(cellsToSelect) + .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') + .classed('sl-cell-inner-masked', false) + .classed('sl-cell-inner-selected', true); + + const rootParent = d3.select(this.rootNode.parentNode); + rootParent.selectAll('.lane-label').classed('lane-label-masked', function() { + return laneLabels.indexOf(d3.select(this).text()) === -1; + }); + + if (swimlaneType === 'viewBy') { + // If selecting a cell in the 'view by' swimlane, indicate the corresponding time in the Overall swimlane. + this.highlightOverall(times); + } + } + + maskIrrelevantSwimlanes(maskAll) { + if (maskAll === true) { + // This selects both overall and viewby swimlane + const allSwimlanes = d3.selectAll('.ml-explorer-swimlane'); + allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true); + allSwimlanes .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') .classed('sl-cell-inner-masked', true); - wrapper - .selectAll( - '.sl-cell-inner.sl-cell-inner-selected,.sl-cell-inner-dragselect.sl-cell-inner-selected' - ) - .classed('sl-cell-inner-selected', false); - - d3.selectAll(cellsToSelect) + } else { + const overallSwimlane = d3.select('.ml-swimlane-overall'); + overallSwimlane.selectAll('.lane-label').classed('lane-label-masked', true); + overallSwimlane .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', false) - .classed('sl-cell-inner-selected', true); + .classed('sl-cell-inner-masked', true); + } + } - const rootParent = d3.select(this.rootNode.parentNode); - rootParent.selectAll('.lane-label').classed('lane-label-masked', function() { - return laneLabels.indexOf(d3.select(this).text()) === -1; - }); + clearSelection() { + // This selects both overall and viewby swimlane + const wrapper = d3.selectAll('.ml-explorer-swimlane'); + + wrapper.selectAll('.lane-label').classed('lane-label-masked', false); + wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false); + wrapper + .selectAll('.sl-cell-inner.sl-cell-inner-selected') + .classed('sl-cell-inner-selected', false); + wrapper + .selectAll('.sl-cell-inner-dragselect.sl-cell-inner-selected') + .classed('sl-cell-inner-selected', false); + wrapper.selectAll('.ds-selected').classed('sl-cell-inner-selected', false); + } - if (swimlaneType === 'viewBy') { - // If selecting a cell in the 'view by' swimlane, indicate the corresponding time in the Overall swimlane. - this.highlightOverall(times); - } - } + renderSwimlane() { + const element = d3.select(this.rootNode.parentNode); - maskIrrelevantSwimlanes(maskAll) { - if (maskAll === true) { - // This selects both overall and viewby swimlane - const allSwimlanes = d3.selectAll('.ml-explorer-swimlane'); - allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true); - allSwimlanes - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', true); - } else { - const overallSwimlane = d3.select('.ml-swimlane-overall'); - overallSwimlane.selectAll('.lane-label').classed('lane-label-masked', true); - overallSwimlane - .selectAll('.sl-cell-inner,.sl-cell-inner-dragselect') - .classed('sl-cell-inner-masked', true); - } + // Consider the setting to support to select a range of cells + if (!ALLOW_CELL_RANGE_SELECTION) { + element.classed(SCSS.mlHideRangeSelection, true); } - clearSelection() { - // This selects both overall and viewby swimlane - const wrapper = d3.selectAll('.ml-explorer-swimlane'); - - wrapper.selectAll('.lane-label').classed('lane-label-masked', false); - wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false); - wrapper - .selectAll('.sl-cell-inner.sl-cell-inner-selected') - .classed('sl-cell-inner-selected', false); - wrapper - .selectAll('.sl-cell-inner-dragselect.sl-cell-inner-selected') - .classed('sl-cell-inner-selected', false); - wrapper.selectAll('.ds-selected').classed('sl-cell-inner-selected', false); + // This getter allows us to fetch the current value in `cellMouseover()`. + // Otherwise it will just refer to the value when `cellMouseover()` was instantiated. + const getCellMouseoverActive = () => this.cellMouseoverActive; + + const { + chartWidth, + filterActive, + maskAll, + TimeBuckets, + swimlaneCellClick, + swimlaneData, + swimlaneType, + selection, + } = this.props; + + const { + laneLabels: lanes, + earliest: startTime, + latest: endTime, + interval: stepSecs, + points, + } = swimlaneData; + + function colorScore(value) { + return getSeverityColor(value); } - renderSwimlane() { - const element = d3.select(this.rootNode.parentNode); + const numBuckets = parseInt((endTime - startTime) / stepSecs); + const cellHeight = 30; + const height = (lanes.length + 1) * cellHeight - 10; + const laneLabelWidth = 170; + + element.style('height', `${height + 20}px`); + const swimlanes = element.select('.ml-swimlanes'); + swimlanes.html(''); + + const cellWidth = Math.floor((chartWidth / numBuckets) * 100) / 100; + + const xAxisWidth = cellWidth * numBuckets; + const xAxisScale = d3.time + .scale() + .domain([new Date(startTime * 1000), new Date(endTime * 1000)]) + .range([0, xAxisWidth]); + + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + timeBuckets.setInterval(`${stepSecs}s`); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + + function cellMouseOverFactory(time, i) { + // Don't use an arrow function here because we need access to `this`, + // which is where d3 supplies a reference to the corresponding DOM element. + return function(lane) { + const bucketScore = getBucketScore(lane, time); + if (bucketScore !== 0) { + cellMouseover(this, lane, bucketScore, i, time); + } + }; + } - // Consider the setting to support to select a range of cells - if (!ALLOW_CELL_RANGE_SELECTION) { - element.classed(SCSS.mlHideRangeSelection, true); + function cellMouseover(target, laneLabel, bucketScore, index, time) { + if (bucketScore === undefined || getCellMouseoverActive() === false) { + return; } - // This getter allows us to fetch the current value in `cellMouseover()`. - // Otherwise it will just refer to the value when `cellMouseover()` was instantiated. - const getCellMouseoverActive = () => this.cellMouseoverActive; - - const { - chartWidth, - filterActive, - maskAll, - TimeBuckets, - swimlaneCellClick, - swimlaneData, - swimlaneType, - selection, - intl, - } = this.props; - - const { - laneLabels: lanes, - earliest: startTime, - latest: endTime, - interval: stepSecs, - points, - } = swimlaneData; - - function colorScore(value) { - return getSeverityColor(value); - } + const displayScore = bucketScore > 1 ? parseInt(bucketScore) : '< 1'; - const numBuckets = parseInt((endTime - startTime) / stepSecs); - const cellHeight = 30; - const height = (lanes.length + 1) * cellHeight - 10; - const laneLabelWidth = 170; - - element.style('height', `${height + 20}px`); - const swimlanes = element.select('.ml-swimlanes'); - swimlanes.html(''); - - const cellWidth = Math.floor((chartWidth / numBuckets) * 100) / 100; - - const xAxisWidth = cellWidth * numBuckets; - const xAxisScale = d3.time - .scale() - .domain([new Date(startTime * 1000), new Date(endTime * 1000)]) - .range([0, xAxisWidth]); - - // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); - timeBuckets.setInterval(`${stepSecs}s`); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - - function cellMouseOverFactory(time, i) { - // Don't use an arrow function here because we need access to `this`, - // which is where d3 supplies a reference to the corresponding DOM element. - return function(lane) { - const bucketScore = getBucketScore(lane, time); - if (bucketScore !== 0) { - cellMouseover(this, lane, bucketScore, i, time); - } - }; - } + // Display date using same format as Kibana visualizations. + const formattedDate = formatHumanReadableDateTime(time * 1000); + const tooltipData = [{ name: formattedDate }]; - function cellMouseover(target, laneLabel, bucketScore, index, time) { - if (bucketScore === undefined || getCellMouseoverActive() === false) { - return; - } + if (swimlaneData.fieldName !== undefined) { + tooltipData.push({ + name: swimlaneData.fieldName, + value: laneLabel, + seriesKey: laneLabel, + yAccessor: 'fieldName', + }); + } + tooltipData.push({ + name: i18n.translate('xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', { + defaultMessage: 'Max anomaly score', + }), + value: displayScore, + color: colorScore(displayScore), + seriesKey: laneLabel, + yAccessor: 'anomaly_score', + }); - const displayScore = bucketScore > 1 ? parseInt(bucketScore) : '< 1'; + const offsets = target.className === 'sl-cell-inner' ? { x: 6, y: 0 } : { x: 8, y: 1 }; + mlChartTooltipService.show(tooltipData, target, { + x: target.offsetWidth + offsets.x, + y: 6 + offsets.y, + }); + } - // Display date using same format as Kibana visualizations. - const formattedDate = formatHumanReadableDateTime(time * 1000); - const tooltipData = [{ name: formattedDate }]; + function cellMouseleave() { + mlChartTooltipService.hide(); + } - if (swimlaneData.fieldName !== undefined) { - tooltipData.push({ - name: swimlaneData.fieldName, - value: laneLabel, - seriesKey: laneLabel, - yAccessor: 'fieldName', + const d3Lanes = swimlanes.selectAll('.lane').data(lanes); + const d3LanesEnter = d3Lanes + .enter() + .append('div') + .classed('lane', true); + + d3LanesEnter + .append('div') + .classed('lane-label', true) + .style('width', `${laneLabelWidth}px`) + .html(label => { + const showFilterContext = filterActive === true && label === 'Overall'; + if (showFilterContext) { + return i18n.translate('xpack.ml.explorer.overallSwimlaneUnfilteredLabel', { + defaultMessage: '{label} (unfiltered)', + values: { label: mlEscape(label) }, }); + } else { + return mlEscape(label); } - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', - defaultMessage: 'Max anomaly score', - }), - value: displayScore, - color: colorScore(displayScore), - seriesKey: laneLabel, - yAccessor: 'anomaly_score', - }); + }) + .on('click', () => { + if (selection && typeof selection.lanes !== 'undefined') { + swimlaneCellClick({}); + } + }) + .each(function() { + if (swimlaneData.fieldName !== undefined) { + d3.select(this) + .on('mouseover', label => { + mlChartTooltipService.show( + [{ skipHeader: true }, { name: swimlaneData.fieldName, value: label }], + this, + { + x: laneLabelWidth, + y: 0, + } + ); + }) + .on('mouseout', () => { + mlChartTooltipService.hide(); + }) + .attr('aria-label', label => `${mlEscape(swimlaneData.fieldName)}: ${mlEscape(label)}`); + } + }); - const offsets = target.className === 'sl-cell-inner' ? { x: 6, y: 0 } : { x: 8, y: 1 }; - mlChartTooltipService.show(tooltipData, target, { - x: target.offsetWidth + offsets.x, - y: 6 + offsets.y, - }); - } + const cellsContainer = d3LanesEnter.append('div').classed('cells-container', true); - function cellMouseleave() { - mlChartTooltipService.hide(); + function getBucketScore(lane, time) { + let bucketScore = 0; + const point = points.find(p => { + return p.value > 0 && p.laneLabel === lane && p.time === time; + }); + if (typeof point !== 'undefined') { + bucketScore = point.value; } + return bucketScore; + } - const d3Lanes = swimlanes.selectAll('.lane').data(lanes); - const d3LanesEnter = d3Lanes - .enter() - .append('div') - .classed('lane', true); - - d3LanesEnter - .append('div') - .classed('lane-label', true) - .style('width', `${laneLabelWidth}px`) - .html(label => { - const showFilterContext = filterActive === true && label === 'Overall'; - if (showFilterContext) { - return i18n.translate('xpack.ml.explorer.overallSwimlaneUnfilteredLabel', { - defaultMessage: '{label} (unfiltered)', - values: { label: mlEscape(label) }, - }); - } else { - return mlEscape(label); - } - }) - .on('click', () => { - if (selection && typeof selection.lanes !== 'undefined') { - swimlaneCellClick({}); - } - }) - .each(function() { - if (swimlaneData.fieldName !== undefined) { - d3.select(this) - .on('mouseover', label => { - mlChartTooltipService.show( - [{ skipHeader: true }, { name: swimlaneData.fieldName, value: label }], - this, - { - x: laneLabelWidth, - y: 0, - } - ); - }) - .on('mouseout', () => { - mlChartTooltipService.hide(); - }) - .attr( - 'aria-label', - label => `${mlEscape(swimlaneData.fieldName)}: ${mlEscape(label)}` - ); - } - }); + // TODO - mark if zoomed in to bucket width? + let time = startTime; + Array(numBuckets || 0) + .fill(null) + .forEach((v, i) => { + const cell = cellsContainer + .append('div') + .classed('sl-cell', true) + .style('width', `${cellWidth}px`) + .attr('data-lane-label', label => mlEscape(label)) + .attr('data-time', time) + .attr('data-bucket-score', lane => { + return getBucketScore(lane, time); + }) + // use a factory here to bind the `time` and `i` values + // of this iteration to the event. + .on('mouseover', cellMouseOverFactory(time, i)) + .on('mouseleave', cellMouseleave) + .each(function(laneLabel) { + this.__clickData__ = { + bucketScore: getBucketScore(laneLabel, time), + laneLabel, + swimlaneType, + time, + }; + }); - const cellsContainer = d3LanesEnter.append('div').classed('cells-container', true); + // calls itself with each() to get access to lane (= d3 data) + cell.append('div').each(function(lane) { + const el = d3.select(this); - function getBucketScore(lane, time) { - let bucketScore = 0; - const point = points.find(p => { - return p.value > 0 && p.laneLabel === lane && p.time === time; - }); - if (typeof point !== 'undefined') { - bucketScore = point.value; - } - return bucketScore; - } + let color = 'none'; + let bucketScore = 0; - // TODO - mark if zoomed in to bucket width? - let time = startTime; - Array(numBuckets || 0) - .fill(null) - .forEach((v, i) => { - const cell = cellsContainer - .append('div') - .classed('sl-cell', true) - .style('width', `${cellWidth}px`) - .attr('data-lane-label', label => mlEscape(label)) - .attr('data-time', time) - .attr('data-bucket-score', lane => { - return getBucketScore(lane, time); - }) - // use a factory here to bind the `time` and `i` values - // of this iteration to the event. - .on('mouseover', cellMouseOverFactory(time, i)) - .on('mouseleave', cellMouseleave) - .each(function(laneLabel) { - this.__clickData__ = { - bucketScore: getBucketScore(laneLabel, time), - laneLabel, - swimlaneType, - time, - }; - }); - - // calls itself with each() to get access to lane (= d3 data) - cell.append('div').each(function(lane) { - const el = d3.select(this); - - let color = 'none'; - let bucketScore = 0; - - const point = points.find(p => { - return p.value > 0 && p.laneLabel === lane && p.time === time; - }); - - if (typeof point !== 'undefined') { - bucketScore = point.value; - color = colorScore(bucketScore); - el.classed('sl-cell-inner', true).style('background-color', color); - } else { - el.classed('sl-cell-inner-dragselect', true); - } + const point = points.find(p => { + return p.value > 0 && p.laneLabel === lane && p.time === time; }); - time += stepSecs; + if (typeof point !== 'undefined') { + bucketScore = point.value; + color = colorScore(bucketScore); + el.classed('sl-cell-inner', true).style('background-color', color); + } else { + el.classed('sl-cell-inner-dragselect', true); + } }); - // ['x-axis'] is just a placeholder so we have an array of 1. - const laneTimes = swimlanes - .selectAll('.time-tick-labels') - .data(['x-axis']) - .enter() - .append('div') - .classed('time-tick-labels', true); - - // height of .time-tick-labels - const svgHeight = 25; - const svg = laneTimes - .append('svg') - .attr('width', chartWidth) - .attr('height', svgHeight); - - const xAxis = d3.svg - .axis() - .scale(xAxisScale) - .ticks(numTicksForDateFormat(chartWidth, xAxisTickFormat)) - .tickFormat(tick => moment(tick).format(xAxisTickFormat)); - - const gAxis = svg - .append('g') - .attr('class', 'x axis') - .call(xAxis); - - // remove overlapping labels - let overlapCheck = 0; - gAxis.selectAll('g.tick').each(function() { - const tick = d3.select(this); - const xTransform = d3.transform(tick.attr('transform')).translate[0]; - const tickWidth = tick - .select('text') - .node() - .getBBox().width; - const xMinOffset = xTransform - tickWidth / 2; - const xMaxOffset = xTransform + tickWidth / 2; - // if the tick label overlaps the previous label - // (or overflows the chart to the left), remove it; - // otherwise pick that label's offset as the new offset to check against - if (xMinOffset < overlapCheck) { - tick.remove(); - } else { - overlapCheck = xTransform + tickWidth / 2; - } - // if the last tick label overflows the chart to the right, remove it - if (xMaxOffset > chartWidth) { - tick.remove(); - } + time += stepSecs; }); - // Check for selection and reselect the corresponding swimlane cell - // if the time range and lane label are still in view. - const selectionState = selection; - const selectedType = _.get(selectionState, 'type', undefined); - const selectionViewByFieldName = _.get(selectionState, 'viewByFieldName', ''); - - // If a selection was done in the other swimlane, add the "masked" classes - // to de-emphasize the swimlane cells. - if (swimlaneType !== selectedType && selectedType !== undefined) { - element.selectAll('.lane-label').classed('lane-label-masked', true); - element.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', true); + // ['x-axis'] is just a placeholder so we have an array of 1. + const laneTimes = swimlanes + .selectAll('.time-tick-labels') + .data(['x-axis']) + .enter() + .append('div') + .classed('time-tick-labels', true); + + // height of .time-tick-labels + const svgHeight = 25; + const svg = laneTimes + .append('svg') + .attr('width', chartWidth) + .attr('height', svgHeight); + + const xAxis = d3.svg + .axis() + .scale(xAxisScale) + .ticks(numTicksForDateFormat(chartWidth, xAxisTickFormat)) + .tickFormat(tick => moment(tick).format(xAxisTickFormat)); + + const gAxis = svg + .append('g') + .attr('class', 'x axis') + .call(xAxis); + + // remove overlapping labels + let overlapCheck = 0; + gAxis.selectAll('g.tick').each(function() { + const tick = d3.select(this); + const xTransform = d3.transform(tick.attr('transform')).translate[0]; + const tickWidth = tick + .select('text') + .node() + .getBBox().width; + const xMinOffset = xTransform - tickWidth / 2; + const xMaxOffset = xTransform + tickWidth / 2; + // if the tick label overlaps the previous label + // (or overflows the chart to the left), remove it; + // otherwise pick that label's offset as the new offset to check against + if (xMinOffset < overlapCheck) { + tick.remove(); + } else { + overlapCheck = xTransform + tickWidth / 2; } + // if the last tick label overflows the chart to the right, remove it + if (xMaxOffset > chartWidth) { + tick.remove(); + } + }); + + // Check for selection and reselect the corresponding swimlane cell + // if the time range and lane label are still in view. + const selectionState = selection; + const selectedType = _.get(selectionState, 'type', undefined); + const selectionViewByFieldName = _.get(selectionState, 'viewByFieldName', ''); + + // If a selection was done in the other swimlane, add the "masked" classes + // to de-emphasize the swimlane cells. + if (swimlaneType !== selectedType && selectedType !== undefined) { + element.selectAll('.lane-label').classed('lane-label-masked', true); + element.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', true); + } - this.props.swimlaneRenderDoneListener(); + this.props.swimlaneRenderDoneListener(); + + if ( + (swimlaneType !== selectedType || + (swimlaneData.fieldName !== undefined && + swimlaneData.fieldName !== selectionViewByFieldName)) && + filterActive === false + ) { + // Not this swimlane which was selected. + return; + } + const cellsToSelect = []; + const selectedLanes = _.get(selectionState, 'lanes', []); + const selectedTimes = _.get(selectionState, 'times', []); + const selectedTimeExtent = d3.extent(selectedTimes); + + selectedLanes.forEach(selectedLane => { if ( - (swimlaneType !== selectedType || - (swimlaneData.fieldName !== undefined && - swimlaneData.fieldName !== selectionViewByFieldName)) && - filterActive === false + lanes.indexOf(selectedLane) > -1 && + selectedTimeExtent[0] >= startTime && + selectedTimeExtent[1] <= endTime ) { - // Not this swimlane which was selected. - return; + // Locate matching cell - look for exact time, otherwise closest before. + const swimlaneElements = element.select('.ml-swimlanes'); + const laneCells = swimlaneElements.selectAll( + `div[data-lane-label="${mlEscape(selectedLane)}"]` + ); + + laneCells.each(function() { + const cell = d3.select(this); + const cellTime = cell.attr('data-time'); + if (cellTime >= selectedTimeExtent[0] && cellTime <= selectedTimeExtent[1]) { + cellsToSelect.push(cell.node()); + } + }); } + }); - const cellsToSelect = []; - const selectedLanes = _.get(selectionState, 'lanes', []); - const selectedTimes = _.get(selectionState, 'times', []); - const selectedTimeExtent = d3.extent(selectedTimes); + const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { + return Math.max(maxBucketScore, +d3.select(cell).attr('data-bucket-score') || 0); + }, 0); - selectedLanes.forEach(selectedLane => { - if ( - lanes.indexOf(selectedLane) > -1 && - selectedTimeExtent[0] >= startTime && - selectedTimeExtent[1] <= endTime - ) { - // Locate matching cell - look for exact time, otherwise closest before. - const swimlaneElements = element.select('.ml-swimlanes'); - const laneCells = swimlaneElements.selectAll( - `div[data-lane-label="${mlEscape(selectedLane)}"]` - ); + const selectedCellTimes = cellsToSelect.map(e => { + return d3.select(e).node().__clickData__.time; + }); - laneCells.each(function() { - const cell = d3.select(this); - const cellTime = cell.attr('data-time'); - if (cellTime >= selectedTimeExtent[0] && cellTime <= selectedTimeExtent[1]) { - cellsToSelect.push(cell.node()); - } - }); - } - }); - - const selectedMaxBucketScore = cellsToSelect.reduce((maxBucketScore, cell) => { - return Math.max(maxBucketScore, +d3.select(cell).attr('data-bucket-score') || 0); - }, 0); - - const selectedCellTimes = cellsToSelect.map(e => { - return d3.select(e).node().__clickData__.time; - }); - - if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { - this.highlightSelection(cellsToSelect, selectedLanes, selectedCellTimes); - } else if (filterActive === true) { - if (selectedCellTimes.length > 0) { - this.highlightOverall(selectedCellTimes); - } - this.maskIrrelevantSwimlanes(maskAll); - } else { - this.clearSelection(); + if (cellsToSelect.length > 1 || selectedMaxBucketScore > 0) { + this.highlightSelection(cellsToSelect, selectedLanes, selectedCellTimes); + } else if (filterActive === true) { + if (selectedCellTimes.length > 0) { + this.highlightOverall(selectedCellTimes); } + this.maskIrrelevantSwimlanes(maskAll); + } else { + this.clearSelection(); } + } - shouldComponentUpdate() { - return true; - } + shouldComponentUpdate() { + return true; + } - setRef(componentNode) { - this.rootNode = componentNode; - } + setRef(componentNode) { + this.rootNode = componentNode; + } - render() { - const { swimlaneType } = this.props; + render() { + const { swimlaneType } = this.props; - return ( -
- ); - } + return ( +
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js index adc740af12057..20a23bcc7968e 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './explorer_swimlane.test.mocks'; import mockOverallSwimlaneData from './__mocks__/mock_overall_swimlane.json'; import moment from 'moment-timezone'; @@ -14,13 +13,6 @@ import React from 'react'; import { dragSelect$ } from './explorer_dashboard_service'; import { ExplorerSwimlane } from './explorer_swimlane'; -jest.mock('ui/chrome', () => ({ - getBasePath: path => path, - getUiSettingsClient: () => ({ - get: jest.fn(), - }), -})); - jest.mock('./explorer_dashboard_service', () => ({ dragSelect$: { subscribe: jest.fn(() => ({ @@ -64,7 +56,7 @@ describe('ExplorerSwimlane', () => { const swimlaneRenderDoneListener = jest.fn(); const wrapper = mountWithIntl( - { const swimlaneRenderDoneListener = jest.fn(); const wrapper = mountWithIntl( - { - return {}; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js index 4818856b8a8d2..0b41f789bb571 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js @@ -12,10 +12,6 @@ import { chain, each, get, union, uniq } from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; - -import { npStart } from 'ui/new_platform'; -import { timefilter } from 'ui/timefilter'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, @@ -31,6 +27,7 @@ import { ml } from '../services/ml_api_service'; import { mlJobService } from '../services/job_service'; import { mlResultsService } from '../services/results_service'; import { getBoundsRoundedToInterval, TimeBuckets } from '../util/time_buckets'; +import { getTimefilter, getUiSettings } from '../util/dependency_cache'; import { MAX_CATEGORY_EXAMPLES, @@ -40,8 +37,6 @@ import { } from './explorer_constants'; import { getSwimlaneContainerWidth } from './legacy_utils'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); - // create new job objects based on standard job config objects // new job objects just contain job id, bucket span in seconds and a selected flag. export function createJobs(jobs) { @@ -149,9 +144,9 @@ export function getInfluencers(selectedJobs = []) { } export function getDateFormatTz() { - const config = npStart.core.uiSettings; + const uiSettings = getUiSettings(); // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. - const tzConfig = config.get('dateFormat:tz'); + const tzConfig = uiSettings.get('dateFormat:tz'); const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); return dateFormatTz; } @@ -238,6 +233,7 @@ export function getSelectionJobIds(selectedCells, selectedJobs) { export function getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth) { // Bucketing interval should be the maximum of the chart related interval (i.e. time range related) // and the max bucket span for the jobs shown in the chart. + const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); const buckets = new TimeBuckets(); buckets.setInterval('auto'); @@ -544,10 +540,6 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, : selectedJobs.map(d => d.id); const timeRange = getSelectionTimeRange(selectedCells, interval, bounds); - if (mlAnnotationsEnabled === false) { - return Promise.resolve([]); - } - return new Promise(resolve => { ml.annotations .getAnnotations({ @@ -816,6 +808,7 @@ export function loadViewBySwimlane( } else { // Ensure the search bounds align to the bucketing interval used in the swimlane so // that the first and last buckets are complete. + const timefilter = getTimefilter(); const timefilterBounds = timefilter.getActiveBounds(); const searchBounds = getBoundsRoundedToInterval( timefilterBounds, diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts b/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts index 992357a82efaa..87a9548a432b1 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore import numeral from '@elastic/numeral'; /** diff --git a/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js deleted file mode 100644 index f0539a5f8c9ab..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; -import { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; - -uiModules.get('xpack/ml').run(() => { - const showAppLink = xpackInfo.get('features.ml.showLinks', false); - - const navLinkUpdates = { - // hide by default, only show once the xpackInfo is initialized - hidden: !showAppLink, - disabled: !showAppLink || (showAppLink && !xpackInfo.get('features.ml.isAvailable', false)), - }; - - npStart.core.chrome.navLinks.update('ml', navLinkUpdates); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx index d78efe632501b..4c0956a46d669 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx @@ -16,10 +16,9 @@ import { EuiTextArea, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; - import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useMlKibana } from '../../../contexts/kibana'; import { isValidLabel, openCustomUrlWindow } from '../../../util/custom_url_utils'; import { getTestUrl } from './utils'; @@ -49,6 +48,9 @@ export interface CustomUrlListProps { * with buttons for testing and deleting each custom URL. */ export const CustomUrlList: FC = ({ job, customUrls, setCustomUrls }) => { + const { + services: { notifications }, + } = useMlKibana(); const [expandedUrlIndex, setExpandedUrlIndex] = useState(null); const onLabelChange = (e: ChangeEvent, index: number) => { @@ -106,7 +108,9 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust .catch(resp => { // eslint-disable-next-line no-console console.error('Error obtaining URL for test:', resp); - toastNotifications.addDanger( + + const { toasts } = notifications; + toasts.addDanger( i18n.translate( 'xpack.ml.customUrlEditorList.obtainingUrlToTestConfigurationErrorMessage', { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js index ef36e84d94d14..cb7c9478244aa 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js @@ -6,7 +6,6 @@ import { TIME_RANGE_TYPE, URL_TYPE } from './constants'; -import chrome from 'ui/chrome'; import rison from 'rison-node'; import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; @@ -16,6 +15,7 @@ import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_ import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; import { escapeForElasticsearchQuery } from '../../../util/string_utils'; +import { getSavedObjectsClient } from '../../../util/dependency_cache'; export function getNewCustomUrlDefaults(job, dashboards, indexPatterns) { // Returns the settings object in the format used by the custom URL editor @@ -133,7 +133,7 @@ function buildDashboardUrlFromSettings(settings) { return new Promise((resolve, reject) => { const { dashboardId, queryFieldNames } = settings.kibanaSettings; - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); savedObjectsClient .get('dashboard', dashboardId) .then(response => { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js index 35e2e73a880d0..15ccba6316e03 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js @@ -19,28 +19,28 @@ import { EuiFlexItem, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { loadFullJob } from '../utils'; import { mlCreateWatchService } from './create_watch_service'; import { CreateWatch } from './create_watch_view'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; -function getSuccessToast(id, url, intl) { +function getSuccessToast(id, url) { return { - title: intl.formatMessage( + title: i18n.translate( + 'xpack.ml.jobsList.createWatchFlyout.watchCreatedSuccessfullyNotificationMessage', { - id: 'xpack.ml.jobsList.createWatchFlyout.watchCreatedSuccessfullyNotificationMessage', defaultMessage: 'Watch {id} created successfully', - }, - { id } + values: { id }, + } ), text: ( - {intl.formatMessage({ - id: 'xpack.ml.jobsList.createWatchFlyout.editWatchButtonLabel', + {i18n.translate('xpack.ml.jobsList.createWatchFlyout.editWatchButtonLabel', { defaultMessage: 'Edit watch', })} @@ -51,7 +51,7 @@ function getSuccessToast(id, url, intl) { }; } -class CreateWatchFlyoutUI extends Component { +export class CreateWatchFlyoutUI extends Component { constructor(props) { super(props); @@ -100,19 +100,21 @@ class CreateWatchFlyoutUI extends Component { }; save = () => { - const { intl } = this.props; + const { toasts } = this.props.kibana.services.notifications; mlCreateWatchService .createNewWatch(this.state.jobId) .then(resp => { - toastNotifications.addSuccess(getSuccessToast(resp.id, resp.url, intl)); + toasts.addSuccess(getSuccessToast(resp.id, resp.url)); this.closeFlyout(true); }) .catch(error => { - toastNotifications.addDanger( - intl.formatMessage({ - id: 'xpack.ml.jobsList.createWatchFlyout.watchNotSavedErrorNotificationMessage', - defaultMessage: 'Could not save watch', - }) + toasts.addDanger( + i18n.translate( + 'xpack.ml.jobsList.createWatchFlyout.watchNotSavedErrorNotificationMessage', + { + defaultMessage: 'Could not save watch', + } + ) ); console.error(error); }); @@ -176,4 +178,4 @@ CreateWatchFlyoutUI.propTypes = { flyoutHidden: PropTypes.func, }; -export const CreateWatchFlyout = injectI18n(CreateWatchFlyoutUI); +export const CreateWatchFlyout = withKibana(CreateWatchFlyoutUI); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js index 5b4a02a7c754f..887afeb3ba818 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import { template } from 'lodash'; import { http } from '../../../../services/http_service'; @@ -12,6 +11,7 @@ import emailBody from './email.html'; import emailInfluencersBody from './email_influencers.html'; import { watch } from './watch.js'; import { i18n } from '@kbn/i18n'; +import { getBasePath, getAppUrl } from '../../../../util/dependency_cache'; const compiledEmailBody = template(emailBody); const compiledEmailInfluencersBody = template(emailInfluencersBody); @@ -38,8 +38,9 @@ function randomNumber(min, max) { } function saveWatch(watchModel) { - const basePath = chrome.addBasePath('/api/watcher'); - const url = `${basePath}/watch/${watchModel.id}`; + const basePath = getBasePath(); + const path = basePath.prepend('/api/watcher'); + const url = `${path}/watch/${watchModel.id}`; return http({ url, @@ -95,7 +96,7 @@ class CreateWatchService { // create the html by adding the variables to the compiled email body. emailSection.send_email.email.body.html = compiledEmailBody({ - serverAddress: chrome.getAppUrl(), + serverAddress: getAppUrl(), influencersSection: this.config.includeInfluencers === true ? compiledEmailInfluencersBody({ @@ -156,11 +157,12 @@ class CreateWatchService { }, }; + const basePath = getBasePath(); if (id !== '') { saveWatch(watchModel) .then(() => { this.status.watch = this.STATUS.SAVED; - this.config.watcherEditURL = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/watcher/watches/watch/${id}/edit?_g=()`; + this.config.watcherEditURL = `${basePath.get()}/app/kibana#/management/elasticsearch/watcher/watches/watch/${id}/edit?_g=()`; resolve({ id, url: this.config.watcherEditURL, @@ -180,8 +182,9 @@ class CreateWatchService { loadWatch(jobId) { const id = `ml-${jobId}`; - const basePath = chrome.addBasePath('/api/watcher'); - const url = `${basePath}/watch/${id}`; + const basePath = getBasePath(); + const path = basePath.prepend('/api/watcher'); + const url = `${path}/watch/${id}`; return http({ url, method: 'GET', diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js index 7a855301885a9..0595ce5caf931 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js @@ -17,7 +17,8 @@ import { EuiCheckbox, EuiFieldText, EuiCallOut } from '@elastic/eui'; import { has } from 'lodash'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { ml } from '../../../../services/ml_api_service'; @@ -25,194 +26,195 @@ import { SelectSeverity } from './select_severity'; import { mlCreateWatchService } from './create_watch_service'; const STATUS = mlCreateWatchService.STATUS; -export const CreateWatch = injectI18n( - class CreateWatch extends Component { - static propTypes = { - jobId: PropTypes.string.isRequired, - bucketSpan: PropTypes.string.isRequired, +export class CreateWatch extends Component { + static propTypes = { + jobId: PropTypes.string.isRequired, + bucketSpan: PropTypes.string.isRequired, + }; + + constructor(props) { + super(props); + mlCreateWatchService.reset(); + this.config = mlCreateWatchService.config; + + this.state = { + jobId: this.props.jobId, + bucketSpan: this.props.bucketSpan, + interval: this.config.interval, + threshold: this.config.threshold, + includeEmail: this.config.emailIncluded, + email: this.config.email, + emailEnabled: false, + status: null, + watchAlreadyExists: false, }; + } - constructor(props) { - super(props); - mlCreateWatchService.reset(); - this.config = mlCreateWatchService.config; - - this.state = { - jobId: this.props.jobId, - bucketSpan: this.props.bucketSpan, - interval: this.config.interval, - threshold: this.config.threshold, - includeEmail: this.config.emailIncluded, - email: this.config.email, - emailEnabled: false, - status: null, - watchAlreadyExists: false, - }; - } - - componentDidMount() { - // make the interval 2 times the bucket span - if (this.state.bucketSpan) { - const intervalObject = parseInterval(this.state.bucketSpan); - let bs = intervalObject.asMinutes() * 2; - if (bs < 1) { - bs = 1; - } - - const interval = `${bs}m`; - this.setState({ interval }, () => { - this.config.interval = interval; - }); + componentDidMount() { + // make the interval 2 times the bucket span + if (this.state.bucketSpan) { + const intervalObject = parseInterval(this.state.bucketSpan); + let bs = intervalObject.asMinutes() * 2; + if (bs < 1) { + bs = 1; } - // load elasticsearch settings to see if email has been configured - ml.getNotificationSettings().then(resp => { - if (has(resp, 'defaults.xpack.notification.email')) { - this.setState({ emailEnabled: true }); - } - }); - - mlCreateWatchService - .loadWatch(this.state.jobId) - .then(() => { - this.setState({ watchAlreadyExists: true }); - }) - .catch(() => { - this.setState({ watchAlreadyExists: false }); - }); - } - - onThresholdChange = threshold => { - this.setState({ threshold }, () => { - this.config.threshold = threshold; - }); - }; - - onIntervalChange = e => { - const interval = e.target.value; + const interval = `${bs}m`; this.setState({ interval }, () => { this.config.interval = interval; }); - }; - - onIncludeEmailChanged = e => { - const includeEmail = e.target.checked; - this.setState({ includeEmail }, () => { - this.config.includeEmail = includeEmail; - }); - }; + } - onEmailChange = e => { - const email = e.target.value; - this.setState({ email }, () => { - this.config.email = email; + // load elasticsearch settings to see if email has been configured + ml.getNotificationSettings().then(resp => { + if (has(resp, 'defaults.xpack.notification.email')) { + this.setState({ emailEnabled: true }); + } + }); + + mlCreateWatchService + .loadWatch(this.state.jobId) + .then(() => { + this.setState({ watchAlreadyExists: true }); + }) + .catch(() => { + this.setState({ watchAlreadyExists: false }); }); - }; - - render() { - const { intl } = this.props; - const { status } = this.state; - - if (status === null || status === STATUS.SAVING || status === STATUS.SAVE_FAILED) { - return ( -
-
-
-
- -
- - ), - }} - /> -
+ } -
-
- -
-
- -
+ onThresholdChange = threshold => { + this.setState({ threshold }, () => { + this.config.threshold = threshold; + }); + }; + + onIntervalChange = e => { + const interval = e.target.value; + this.setState({ interval }, () => { + this.config.interval = interval; + }); + }; + + onIncludeEmailChanged = e => { + const includeEmail = e.target.checked; + this.setState({ includeEmail }, () => { + this.config.includeEmail = includeEmail; + }); + }; + + onEmailChange = e => { + const email = e.target.value; + this.setState({ email }, () => { + this.config.email = email; + }); + }; + + render() { + const { status } = this.state; + + if (status === null || status === STATUS.SAVING || status === STATUS.SAVE_FAILED) { + return ( +
+
+
+
+
-
- {this.state.emailEnabled && ( -
- - } - checked={this.state.includeEmail} - onChange={this.onIncludeEmailChanged} - /> - {this.state.includeEmail && ( -
+ -
- )} + ), + }} + /> +
+ +
+
+ +
+
+
- )} - {this.state.watchAlreadyExists && ( - +
+ {this.state.emailEnabled && ( +
+ } + checked={this.state.includeEmail} + onChange={this.onIncludeEmailChanged} /> - )} -
- ); - } else if (status === STATUS.SAVED) { - return ( -
- + +
+ )} +
+ )} + {this.state.watchAlreadyExists && ( + + } /> -
- ); - } else { - return
; - } + )} +
+ ); + } else if (status === STATUS.SAVED) { + return ( +
+ +
+ ); + } else { + return
; } } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js index 67398974447f9..3e129a174c9e0 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js @@ -17,160 +17,160 @@ import { import { deleteJobs } from '../utils'; import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; - -export const DeleteJobModal = injectI18n( - class extends Component { - static displayName = 'DeleteJobModal'; - static propTypes = { - setShowFunction: PropTypes.func.isRequired, - unsetShowFunction: PropTypes.func.isRequired, - refreshJobs: PropTypes.func.isRequired, +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export class DeleteJobModal extends Component { + static displayName = 'DeleteJobModal'; + static propTypes = { + setShowFunction: PropTypes.func.isRequired, + unsetShowFunction: PropTypes.func.isRequired, + refreshJobs: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + jobs: [], + isModalVisible: false, + deleting: false, }; - constructor(props) { - super(props); - - this.state = { - jobs: [], - isModalVisible: false, - deleting: false, - }; + this.refreshJobs = this.props.refreshJobs; + } - this.refreshJobs = this.props.refreshJobs; + componentDidMount() { + if (typeof this.props.setShowFunction === 'function') { + this.props.setShowFunction(this.showModal); } + } - componentDidMount() { - if (typeof this.props.setShowFunction === 'function') { - this.props.setShowFunction(this.showModal); - } + componentWillUnmount() { + if (typeof this.props.unsetShowFunction === 'function') { + this.props.unsetShowFunction(); } + } - componentWillUnmount() { - if (typeof this.props.unsetShowFunction === 'function') { - this.props.unsetShowFunction(); - } + closeModal = () => { + this.setState({ isModalVisible: false }); + }; + + showModal = jobs => { + this.setState({ + jobs, + isModalVisible: true, + deleting: false, + }); + }; + + deleteJob = () => { + this.setState({ deleting: true }); + deleteJobs(this.state.jobs); + + setTimeout(() => { + this.closeModal(); + this.refreshJobs(); + }, DELETING_JOBS_REFRESH_INTERVAL_MS); + }; + + setEL = el => { + if (el) { + this.el = el; } - - closeModal = () => { - this.setState({ isModalVisible: false }); - }; - - showModal = jobs => { - this.setState({ - jobs, - isModalVisible: true, - deleting: false, - }); - }; - - deleteJob = () => { - this.setState({ deleting: true }); - deleteJobs(this.state.jobs); - - setTimeout(() => { - this.closeModal(); - this.refreshJobs(); - }, DELETING_JOBS_REFRESH_INTERVAL_MS); - }; - - setEL = el => { - if (el) { - this.el = el; - } - }; - - render() { - const { intl } = this.props; - let modal; - - if (this.state.isModalVisible) { - if (this.el && this.state.deleting === true) { - // work around to disable the modal's buttons if the jobs are being deleted - this.el.confirmButton.style.display = 'none'; - this.el.cancelButton.textContent = intl.formatMessage({ - id: 'xpack.ml.jobsList.deleteJobModal.closeButtonLabel', + }; + + render() { + let modal; + + if (this.state.isModalVisible) { + if (this.el && this.state.deleting === true) { + // work around to disable the modal's buttons if the jobs are being deleted + this.el.confirmButton.style.display = 'none'; + this.el.cancelButton.textContent = i18n.translate( + 'xpack.ml.jobsList.deleteJobModal.closeButtonLabel', + { defaultMessage: 'Close', - }); - } - - const title = ( - + } ); - modal = ( - - - } - confirmButtonText={ + } + + const title = ( + + ); + modal = ( + + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + className="eui-textBreakWord" + > + {this.state.deleting === true && ( +
- } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - className="eui-textBreakWord" - > - {this.state.deleting === true && ( -
+ +
+ +
+
+ )} + + {this.state.deleting === false && ( + +

- -

- -
-
- )} - - {this.state.deleting === false && ( - -

- -

-

- -

-
- )} -
-
- ); - } - - return
{modal}
; + values={{ + jobsCount: this.state.jobs.length, + }} + /> +

+ + )} +
+
+ ); } + + return
{modal}
; } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 3e100ed8637ad..7c1639395e02e 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -27,10 +27,11 @@ import { saveJob } from './edit_utils'; import { loadFullJob } from '../utils'; import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from '../validate_job'; import { mlMessageBarService } from '../../../../components/messagebar'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -class EditJobFlyoutUI extends Component { +export class EditJobFlyoutUI extends Component { _initialJobFormState = null; constructor(props) { @@ -175,11 +176,13 @@ class EditJobFlyoutUI extends Component { if (jobDetails.jobGroups !== undefined) { if (jobDetails.jobGroups.some(j => this.props.allJobIds.includes(j))) { - jobGroupsValidationError = this.props.intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage', - defaultMessage: - 'A job with this ID already exists. Groups and jobs cannot use the same ID.', - }); + jobGroupsValidationError = i18n.translate( + 'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage', + { + defaultMessage: + 'A job with this ID already exists. Groups and jobs cannot use the same ID.', + } + ); } else { jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message; } @@ -229,34 +232,29 @@ class EditJobFlyoutUI extends Component { customUrls: this.state.jobCustomUrls, }; + const { toasts } = this.props.kibana.services.notifications; saveJob(this.state.job, newJobData) .then(() => { - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: 'xpack.ml.jobsList.editJobFlyout.changesSavedNotificationMessage', - defaultMessage: 'Changes to {jobId} saved', - }, - { + toasts.addSuccess( + i18n.translate('xpack.ml.jobsList.editJobFlyout.changesSavedNotificationMessage', { + defaultMessage: 'Changes to {jobId} saved', + values: { jobId: this.state.job.job_id, - } - ) + }, + }) ); this.refreshJobs(); this.closeFlyout(true); }) .catch(error => { console.error(error); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.ml.jobsList.editJobFlyout.changesNotSavedNotificationMessage', - defaultMessage: 'Could not save changes to {jobId}', - }, - { + toasts.addDanger( + i18n.translate('xpack.ml.jobsList.editJobFlyout.changesNotSavedNotificationMessage', { + defaultMessage: 'Could not save changes to {jobId}', + values: { jobId: this.state.job.job_id, - } - ) + }, + }) ); mlMessageBarService.notify.error(error); }); @@ -286,13 +284,10 @@ class EditJobFlyoutUI extends Component { isValidJobCustomUrls, } = this.state; - const { intl } = this.props; - const tabs = [ { id: 'job-details', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.jobDetailsTitle', + name: i18n.translate('xpack.ml.jobsList.editJobFlyout.jobDetailsTitle', { defaultMessage: 'Job details', }), content: ( @@ -308,8 +303,7 @@ class EditJobFlyoutUI extends Component { }, { id: 'detectors', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.detectorsTitle', + name: i18n.translate('xpack.ml.jobsList.editJobFlyout.detectorsTitle', { defaultMessage: 'Detectors', }), content: ( @@ -322,8 +316,7 @@ class EditJobFlyoutUI extends Component { }, { id: 'datafeed', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.datafeedTitle', + name: i18n.translate('xpack.ml.jobsList.editJobFlyout.datafeedTitle', { defaultMessage: 'Datafeed', }), content: ( @@ -339,8 +332,7 @@ class EditJobFlyoutUI extends Component { }, { id: 'custom-urls', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.editJobFlyout.customUrlsTitle', + name: i18n.translate('xpack.ml.jobsList.editJobFlyout.customUrlsTitle', { defaultMessage: 'Custom URLs', }), content: ( @@ -463,4 +455,4 @@ EditJobFlyoutUI.propTypes = { allJobIds: PropTypes.array.isRequired, }; -export const EditJobFlyout = injectI18n(EditJobFlyoutUI); +export const EditJobFlyout = withKibana(EditJobFlyoutUI); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js index 0c8b7131c3447..a49a2af896be2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js @@ -5,10 +5,10 @@ */ import { difference } from 'lodash'; -import chrome from 'ui/chrome'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { mlJobService } from '../../../../services/job_service'; import { processCreatedBy } from '../../../../../../common/util/job_utils'; +import { getSavedObjectsClient } from '../../../../util/dependency_cache'; export function saveJob(job, newJobData, finish) { return new Promise((resolve, reject) => { @@ -77,7 +77,7 @@ function saveDatafeed(datafeedData, job) { export function loadSavedDashboards(maxNumber) { // Loads the list of saved dashboards, as used in editing custom URLs. return new Promise((resolve, reject) => { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); savedObjectsClient .find({ type: 'dashboard', @@ -109,7 +109,7 @@ export function loadIndexPatterns(maxNumber) { // TODO - amend loadIndexPatterns in index_utils.js to do the request, // without needing an Angular Provider. return new Promise((resolve, reject) => { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); savedObjectsClient .find({ type: 'index-pattern', diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index c36b4ceed7d57..fe6f72fd10279 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -20,7 +20,6 @@ import { EuiModalHeaderTitle, EuiModalFooter, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -33,11 +32,13 @@ import { getTestUrl, CustomUrlSettings, } from '../../../../components/custom_url_editor/utils'; +import { withKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { loadSavedDashboards, loadIndexPatterns } from '../edit_utils'; import { openCustomUrlWindow } from '../../../../../util/custom_url_utils'; import { Job } from '../../../../new_job/common/job_creator/configs'; import { UrlConfig } from '../../../../../../../common/types/custom_urls'; import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; +import { MlKibanaReactContextValue } from '../../../../../contexts/kibana'; const MAX_NUMBER_DASHBOARDS = 1000; const MAX_NUMBER_INDEX_PATTERNS = 1000; @@ -47,6 +48,7 @@ interface CustomUrlsProps { jobCustomUrls: UrlConfig[]; setCustomUrls: (customUrls: UrlConfig[]) => void; editMode: 'inline' | 'modal'; + kibana: MlKibanaReactContextValue; } interface CustomUrlsState { @@ -58,7 +60,7 @@ interface CustomUrlsState { editorSettings?: CustomUrlSettings; } -export class CustomUrls extends Component { +class CustomUrlsUI extends Component { constructor(props: CustomUrlsProps) { super(props); @@ -80,6 +82,7 @@ export class CustomUrls extends Component { } componentDidMount() { + const { toasts } = this.props.kibana.services.notifications; loadSavedDashboards(MAX_NUMBER_DASHBOARDS) .then(dashboards => { this.setState({ dashboards }); @@ -87,7 +90,7 @@ export class CustomUrls extends Component { .catch(resp => { // eslint-disable-next-line no-console console.error('Error loading list of dashboards:', resp); - toastNotifications.addDanger( + toasts.addDanger( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.loadSavedDashboardsErrorNotificationMessage', { @@ -104,7 +107,7 @@ export class CustomUrls extends Component { .catch(resp => { // eslint-disable-next-line no-console console.error('Error loading list of dashboards:', resp); - toastNotifications.addDanger( + toasts.addDanger( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.loadIndexPatternsErrorNotificationMessage', { @@ -143,7 +146,8 @@ export class CustomUrls extends Component { .catch((error: any) => { // eslint-disable-next-line no-console console.error('Error building custom URL from settings:', error); - toastNotifications.addDanger( + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.addNewUrlErrorNotificationMessage', { @@ -156,6 +160,7 @@ export class CustomUrls extends Component { }; onTestButtonClick = () => { + const { toasts } = this.props.kibana.services.notifications; const job = this.props.job; buildCustomUrlFromSettings(this.state.editorSettings as CustomUrlSettings) .then(customUrl => { @@ -166,7 +171,7 @@ export class CustomUrls extends Component { .catch(resp => { // eslint-disable-next-line no-console console.error('Error obtaining URL for test:', resp); - toastNotifications.addWarning( + toasts.addWarning( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.getTestUrlErrorNotificationMessage', { @@ -179,7 +184,7 @@ export class CustomUrls extends Component { .catch(resp => { // eslint-disable-next-line no-console console.error('Error building custom URL from settings:', resp); - toastNotifications.addWarning( + toasts.addWarning( i18n.translate( 'xpack.ml.jobsList.editJobFlyout.customUrls.buildUrlErrorNotificationMessage', { @@ -330,3 +335,5 @@ export class CustomUrls extends Component { ); } } + +export const CustomUrls = withKibana(CustomUrlsUI); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js index ab2658c0dc124..a609d6a7c3fba 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js @@ -10,9 +10,10 @@ import React, { Component } from 'react'; import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiComboBox } from '@elastic/eui'; import { ml } from '../../../../../services/ml_api_service'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -class JobDetailsUI extends Component { +export class JobDetails extends Component { constructor(props) { super(props); @@ -129,10 +130,12 @@ class JobDetailsUI extends Component { error={groupsValidationError} > ); } -ResultLinksUI.propTypes = { +ResultLinks.propTypes = { jobs: PropTypes.array.isRequired, }; - -export const ResultLinks = injectI18n(ResultLinksUI); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index e70198b36e0df..41dfdb0dcfeed 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -23,7 +23,8 @@ import { formatDate, formatNumber } from '@elastic/eui/lib/services/format'; import { FORECAST_REQUEST_STATE } from '../../../../../../../common/constants/states'; import { addItemToRecentlyAccessed } from '../../../../../util/recently_accessed'; import { mlForecastService } from '../../../../../services/forecast_service'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob, @@ -35,7 +36,7 @@ const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; /** * Table component for rendering the lists of forecasts run on an ML job. */ -class ForecastsTableUI extends Component { +export class ForecastsTable extends Component { constructor(props) { super(props); this.state = { @@ -65,10 +66,12 @@ class ForecastsTableUI extends Component { console.log('Error loading list of forecasts for jobs list:', resp); this.setState({ isLoading: false, - errorMessage: this.props.intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.loadingErrorMessage', - defaultMessage: 'Error loading the list of forecasts run on this job', - }), + errorMessage: i18n.translate( + 'xpack.ml.jobsList.jobDetails.forecastsTable.loadingErrorMessage', + { + defaultMessage: 'Error loading the list of forecasts run on this job', + } + ), forecasts: [], }); }); @@ -191,13 +194,10 @@ class ForecastsTableUI extends Component { ); } - const { intl } = this.props; - const columns = [ { field: 'forecast_create_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.createdLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.createdLabel', { defaultMessage: 'Created', }), dataType: 'date', @@ -208,8 +208,7 @@ class ForecastsTableUI extends Component { }, { field: 'forecast_start_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.fromLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.fromLabel', { defaultMessage: 'From', }), dataType: 'date', @@ -219,8 +218,7 @@ class ForecastsTableUI extends Component { }, { field: 'forecast_end_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.toLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.toLabel', { defaultMessage: 'To', }), dataType: 'date', @@ -230,16 +228,14 @@ class ForecastsTableUI extends Component { }, { field: 'forecast_status', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel', { defaultMessage: 'Status', }), sortable: true, }, { field: 'forecast_memory_bytes', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.memorySizeLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.memorySizeLabel', { defaultMessage: 'Memory size', }), render: bytes => formatNumber(bytes, '0b'), @@ -247,26 +243,21 @@ class ForecastsTableUI extends Component { }, { field: 'processing_time_ms', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel', { defaultMessage: 'Processing time', }), render: ms => - intl.formatMessage( - { - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.msTimeUnitLabel', - defaultMessage: '{ms} ms', - }, - { + i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.msTimeUnitLabel', { + defaultMessage: '{ms} ms', + values: { ms, - } - ), + }, + }), sortable: true, }, { field: 'forecast_expiry_timestamp', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.expiresLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.expiresLabel', { defaultMessage: 'Expires', }), render: date => formatDate(date, TIME_FORMAT), @@ -275,8 +266,7 @@ class ForecastsTableUI extends Component { }, { field: 'forecast_messages', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.messagesLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.messagesLabel', { defaultMessage: 'Messages', }), sortable: false, @@ -292,19 +282,18 @@ class ForecastsTableUI extends Component { textOnly: true, }, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel', { defaultMessage: 'View', }), width: '60px', render: forecast => { - const viewForecastAriaLabel = intl.formatMessage( + const viewForecastAriaLabel = i18n.translate( + 'xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel', { - id: 'xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel', defaultMessage: 'View forecast created at {createdDate}', - }, - { - createdDate: formatDate(forecast.forecast_create_timestamp, TIME_FORMAT), + values: { + createdDate: formatDate(forecast.forecast_create_timestamp, TIME_FORMAT), + }, } ); @@ -333,10 +322,6 @@ class ForecastsTableUI extends Component { ); } } -ForecastsTableUI.propTypes = { +ForecastsTable.propTypes = { job: PropTypes.object.isRequired, }; - -const ForecastsTable = injectI18n(ForecastsTableUI); - -export { ForecastsTable }; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index 69891ce0cd2fe..e3f348ad32b0c 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -17,12 +17,9 @@ import { AnnotationFlyout } from '../../../../components/annotations/annotation_ import { ForecastsTable } from './forecasts_table'; import { JobDetailsPane } from './job_details_pane'; import { JobMessagesPane } from './job_messages_pane'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); - -class JobDetailsUI extends Component { +export class JobDetails extends Component { constructor(props) { super(props); @@ -66,14 +63,13 @@ class JobDetailsUI extends Component { datafeedTimingStats, } = extractJobDetails(job); - const { intl, showFullDetails } = this.props; + const { showFullDetails } = this.props; const tabs = [ { id: 'job-settings', 'data-test-subj': 'mlJobListTab-job-settings', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.jobSettingsLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.jobSettingsLabel', { defaultMessage: 'Job settings', }), content: ( @@ -87,8 +83,7 @@ class JobDetailsUI extends Component { { id: 'job-config', 'data-test-subj': 'mlJobListTab-job-config', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.jobConfigLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.jobConfigLabel', { defaultMessage: 'Job config', }), content: ( @@ -101,8 +96,7 @@ class JobDetailsUI extends Component { { id: 'counts', 'data-test-subj': 'mlJobListTab-counts', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.countsLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.countsLabel', { defaultMessage: 'Counts', }), content: ( @@ -115,8 +109,7 @@ class JobDetailsUI extends Component { { id: 'json', 'data-test-subj': 'mlJobListTab-json', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.jsonLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.jsonLabel', { defaultMessage: 'JSON', }), content: , @@ -124,8 +117,7 @@ class JobDetailsUI extends Component { { id: 'job-messages', 'data-test-subj': 'mlJobListTab-job-messages', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.jobMessagesLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.jobMessagesLabel', { defaultMessage: 'Job messages', }), content: , @@ -137,8 +129,7 @@ class JobDetailsUI extends Component { tabs.splice(2, 0, { id: 'datafeed', 'data-test-subj': 'mlJobListTab-datafeed', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', { defaultMessage: 'Datafeed', }), content: ( @@ -153,8 +144,7 @@ class JobDetailsUI extends Component { { id: 'datafeed-preview', 'data-test-subj': 'mlJobListTab-datafeed-preview', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.datafeedPreviewLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedPreviewLabel', { defaultMessage: 'Datafeed preview', }), content: , @@ -162,8 +152,7 @@ class JobDetailsUI extends Component { { id: 'forecasts', 'data-test-subj': 'mlJobListTab-forecasts', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.forecastsLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.forecastsLabel', { defaultMessage: 'Forecasts', }), content: , @@ -171,12 +160,11 @@ class JobDetailsUI extends Component { ); } - if (mlAnnotationsEnabled && showFullDetails) { + if (showFullDetails) { tabs.push({ id: 'annotations', 'data-test-subj': 'mlJobListTab-annotations', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.tabs.annotationsLabel', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.annotationsLabel', { defaultMessage: 'Annotations', }), content: ( @@ -196,12 +184,10 @@ class JobDetailsUI extends Component { } } } -JobDetailsUI.propTypes = { +JobDetails.propTypes = { jobId: PropTypes.string.isRequired, job: PropTypes.object, addYourself: PropTypes.func.isRequired, removeYourself: PropTypes.func.isRequired, showFullDetails: PropTypes.bool, }; - -export const JobDetails = injectI18n(JobDetailsUI); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js index 1ad0e2851dedc..a91df3cce01f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js @@ -12,8 +12,8 @@ import { JobGroup } from '../job_group'; import { getSelectedJobIdFromUrl, clearSelectedJobIdFromUrl } from '../utils'; import { EuiSearchBar, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; function loadGroups() { return ml.jobs @@ -42,7 +42,7 @@ function loadGroups() { }); } -class JobFilterBarUI extends Component { +export class JobFilterBar extends Component { constructor(props) { super(props); @@ -87,7 +87,6 @@ class JobFilterBarUI extends Component { }; render() { - const { intl } = this.props; const { error, selectedId } = this.state; const filters = [ { @@ -96,22 +95,19 @@ class JobFilterBarUI extends Component { items: [ { value: 'opened', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.openedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.openedLabel', { defaultMessage: 'Opened', }), }, { value: 'closed', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.closedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.closedLabel', { defaultMessage: 'Closed', }), }, { value: 'failed', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.failedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.failedLabel', { defaultMessage: 'Failed', }), }, @@ -123,15 +119,13 @@ class JobFilterBarUI extends Component { items: [ { value: 'started', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.startedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.startedLabel', { defaultMessage: 'Started', }), }, { value: 'stopped', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.stoppedLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.stoppedLabel', { defaultMessage: 'Stopped', }), }, @@ -140,8 +134,7 @@ class JobFilterBarUI extends Component { { type: 'field_value_selection', field: 'groups', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobFilterBar.groupLabel', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.groupLabel', { defaultMessage: 'Group', }), multiSelect: 'or', @@ -188,7 +181,7 @@ class JobFilterBarUI extends Component { ); } } -JobFilterBarUI.propTypes = { +JobFilterBar.propTypes = { setFilters: PropTypes.func.isRequired, }; @@ -202,5 +195,3 @@ function getError(error) { return ''; } - -export const JobFilterBar = injectI18n(JobFilterBarUI); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index b691bc34295c5..7036b4f64b3c5 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -17,15 +17,15 @@ import { JobIcon } from '../../../../components/job_message_icon'; import { getJobIdUrl } from '../utils'; import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; // 'isManagementTable' bool prop to determine when to configure table for use in Kibana management page -class JobsListUI extends Component { +export class JobsList extends Component { constructor(props) { super(props); @@ -99,7 +99,7 @@ class JobsListUI extends Component { } render() { - const { intl, loading, isManagementTable } = this.props; + const { loading, isManagementTable } = this.props; const selectionControls = { selectable: job => job.deleting !== true, selectableMessage: (selectable, rowItem) => @@ -141,20 +141,14 @@ class JobsListUI extends Component { iconType={this.state.itemIdToExpandedRowMap[item.id] ? 'arrowDown' : 'arrowRight'} aria-label={ this.state.itemIdToExpandedRowMap[item.id] - ? intl.formatMessage( - { - id: 'xpack.ml.jobsList.collapseJobDetailsAriaLabel', - defaultMessage: 'Hide details for {itemId}', - }, - { itemId: item.id } - ) - : intl.formatMessage( - { - id: 'xpack.ml.jobsList.expandJobDetailsAriaLabel', - defaultMessage: 'Show details for {itemId}', - }, - { itemId: item.id } - ) + ? i18n.translate('xpack.ml.jobsList.collapseJobDetailsAriaLabel', { + defaultMessage: 'Hide details for {itemId}', + values: { itemId: item.id }, + }) + : i18n.translate('xpack.ml.jobsList.expandJobDetailsAriaLabel', { + defaultMessage: 'Show details for {itemId}', + values: { itemId: item.id }, + }) } data-row-id={item.id} data-test-subj="mlJobListRowDetailsToggle" @@ -165,8 +159,7 @@ class JobsListUI extends Component { { field: 'id', 'data-test-subj': 'mlJobListColumnId', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.idLabel', + name: i18n.translate('xpack.ml.jobsList.idLabel', { defaultMessage: 'ID', }), sortable: true, @@ -190,8 +183,7 @@ class JobsListUI extends Component { render: item => , }, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.descriptionLabel', + name: i18n.translate('xpack.ml.jobsList.descriptionLabel', { defaultMessage: 'Description', }), sortable: true, @@ -204,8 +196,7 @@ class JobsListUI extends Component { { field: 'processed_record_count', 'data-test-subj': 'mlJobListColumnRecordCount', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.processedRecordsLabel', + name: i18n.translate('xpack.ml.jobsList.processedRecordsLabel', { defaultMessage: 'Processed records', }), sortable: true, @@ -217,8 +208,7 @@ class JobsListUI extends Component { { field: 'memory_status', 'data-test-subj': 'mlJobListColumnMemoryStatus', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.memoryStatusLabel', + name: i18n.translate('xpack.ml.jobsList.memoryStatusLabel', { defaultMessage: 'Memory status', }), sortable: true, @@ -228,8 +218,7 @@ class JobsListUI extends Component { { field: 'jobState', 'data-test-subj': 'mlJobListColumnJobState', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobStateLabel', + name: i18n.translate('xpack.ml.jobsList.jobStateLabel', { defaultMessage: 'Job state', }), sortable: true, @@ -239,8 +228,7 @@ class JobsListUI extends Component { { field: 'datafeedState', 'data-test-subj': 'mlJobListColumnDatafeedState', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.datafeedStateLabel', + name: i18n.translate('xpack.ml.jobsList.datafeedStateLabel', { defaultMessage: 'Datafeed state', }), sortable: true, @@ -248,8 +236,7 @@ class JobsListUI extends Component { width: '8%', }, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.actionsLabel', + name: i18n.translate('xpack.ml.jobsList.actionsLabel', { defaultMessage: 'Actions', }), render: item => , @@ -259,8 +246,7 @@ class JobsListUI extends Component { if (isManagementTable === true) { // insert before last column columns.splice(columns.length - 1, 0, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.spacesLabel', + name: i18n.translate('xpack.ml.jobsList.spacesLabel', { defaultMessage: 'Spaces', }), render: () => {'all'}, @@ -272,8 +258,7 @@ class JobsListUI extends Component { } else { // insert before last column columns.splice(columns.length - 1, 0, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.latestTimestampLabel', + name: i18n.translate('xpack.ml.jobsList.latestTimestampLabel', { defaultMessage: 'Latest timestamp', }), truncateText: false, @@ -341,12 +326,10 @@ class JobsListUI extends Component { loading={loading === true} noItemsMessage={ loading - ? intl.formatMessage({ - id: 'xpack.ml.jobsList.loadingJobsLabel', + ? i18n.translate('xpack.ml.jobsList.loadingJobsLabel', { defaultMessage: 'Loading jobs…', }) - : intl.formatMessage({ - id: 'xpack.ml.jobsList.noJobsFoundLabel', + : i18n.translate('xpack.ml.jobsList.noJobsFoundLabel', { defaultMessage: 'No jobs found', }) } @@ -368,7 +351,7 @@ class JobsListUI extends Component { ); } } -JobsListUI.propTypes = { +JobsList.propTypes = { jobsSummaryList: PropTypes.array.isRequired, fullJobsList: PropTypes.object.isRequired, isManagementTable: PropTypes.bool, @@ -383,10 +366,8 @@ JobsListUI.propTypes = { selectedJobsCount: PropTypes.number.isRequired, loading: PropTypes.bool, }; -JobsListUI.defaultProps = { +JobsList.defaultProps = { isManagementTable: false, isMlEnabledInSpace: true, loading: false, }; - -export const JobsList = injectI18n(JobsListUI); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index 7d3a9bb878cc1..a5509c0f79a36 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -12,7 +12,8 @@ import React, { Component } from 'react'; import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui'; import { closeJobs, stopDatafeeds, isStartable, isStoppable, isClosable } from '../utils'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; class MultiJobActionsMenuUI extends Component { constructor(props) { @@ -46,10 +47,12 @@ class MultiJobActionsMenuUI extends Component { size="s" onClick={this.onButtonClick} iconType="gear" - aria-label={this.props.intl.formatMessage({ - id: 'xpack.ml.jobsList.multiJobActionsMenu.managementActionsAriaLabel', - defaultMessage: 'Management actions', - })} + aria-label={i18n.translate( + 'xpack.ml.jobsList.multiJobActionsMenu.managementActionsAriaLabel', + { + defaultMessage: 'Management actions', + } + )} color="text" disabled={ anyJobsDeleting || (this.canDeleteJob === false && this.canStartStopDatafeed === false) @@ -155,4 +158,4 @@ MultiJobActionsMenuUI.propTypes = { refreshJobs: PropTypes.func.isRequired, }; -export const MultiJobActionsMenu = injectI18n(MultiJobActionsMenuUI); +export const MultiJobActionsMenu = MultiJobActionsMenuUI; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index 8c49f60d058f8..5f91ba9b6f107 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../../privilege/check_privilege'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; @@ -23,10 +22,12 @@ import { import { cloneDeep } from 'lodash'; import { ml } from '../../../../../services/ml_api_service'; +import { checkPermission } from '../../../../../privilege/check_privilege'; import { GroupList } from './group_list'; import { NewGroupInput } from './new_group_input'; import { mlMessageBarService } from '../../../../../components/messagebar'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; function createSelectedGroups(jobs, groups) { const jobIds = jobs.map(j => j.id); @@ -52,220 +53,219 @@ function createSelectedGroups(jobs, groups) { return selectedGroups; } -export const GroupSelector = injectI18n( - class GroupSelector extends Component { - static propTypes = { - jobs: PropTypes.array.isRequired, - allJobIds: PropTypes.array.isRequired, - refreshJobs: PropTypes.func.isRequired, - }; +export class GroupSelector extends Component { + static propTypes = { + jobs: PropTypes.array.isRequired, + allJobIds: PropTypes.array.isRequired, + refreshJobs: PropTypes.func.isRequired, + }; - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - isPopoverOpen: false, - groups: [], - selectedGroups: {}, - edited: false, - }; + this.state = { + isPopoverOpen: false, + groups: [], + selectedGroups: {}, + edited: false, + }; - this.refreshJobs = this.props.refreshJobs; - this.canUpdateJob = checkPermission('canUpdateJob'); - } + this.refreshJobs = this.props.refreshJobs; + this.canUpdateJob = checkPermission('canUpdateJob'); + } - static getDerivedStateFromProps(props, state) { - if (state.edited === false) { - const selectedGroups = createSelectedGroups(props.jobs, state.groups); - return { selectedGroups }; - } else { - return {}; - } + static getDerivedStateFromProps(props, state) { + if (state.edited === false) { + const selectedGroups = createSelectedGroups(props.jobs, state.groups); + return { selectedGroups }; + } else { + return {}; } + } - togglePopover = () => { - if (this.state.isPopoverOpen) { - this.closePopover(); - } else { - ml.jobs - .groups() - .then(groups => { - const selectedGroups = createSelectedGroups(this.props.jobs, groups); + togglePopover = () => { + if (this.state.isPopoverOpen) { + this.closePopover(); + } else { + ml.jobs + .groups() + .then(groups => { + const selectedGroups = createSelectedGroups(this.props.jobs, groups); - this.setState({ - isPopoverOpen: true, - edited: false, - selectedGroups, - groups, - }); - }) - .catch(error => { - console.error(error); + this.setState({ + isPopoverOpen: true, + edited: false, + selectedGroups, + groups, }); - } - }; + }) + .catch(error => { + console.error(error); + }); + } + }; - closePopover = () => { - this.setState({ - edited: false, - isPopoverOpen: false, - }); - }; + closePopover = () => { + this.setState({ + edited: false, + isPopoverOpen: false, + }); + }; - selectGroup = group => { - const newSelectedGroups = cloneDeep(this.state.selectedGroups); + selectGroup = group => { + const newSelectedGroups = cloneDeep(this.state.selectedGroups); - if (newSelectedGroups[group.id] === undefined) { - newSelectedGroups[group.id] = { - partial: false, - }; - } else if (newSelectedGroups[group.id].partial === true) { - newSelectedGroups[group.id].partial = false; - } else { - delete newSelectedGroups[group.id]; - } + if (newSelectedGroups[group.id] === undefined) { + newSelectedGroups[group.id] = { + partial: false, + }; + } else if (newSelectedGroups[group.id].partial === true) { + newSelectedGroups[group.id].partial = false; + } else { + delete newSelectedGroups[group.id]; + } - this.setState({ - selectedGroups: newSelectedGroups, - edited: true, - }); - }; + this.setState({ + selectedGroups: newSelectedGroups, + edited: true, + }); + }; - applyChanges = () => { - const { selectedGroups } = this.state; - const { jobs } = this.props; - const newJobs = jobs.map(j => ({ - id: j.id, - oldGroups: j.groups, - newGroups: [], - })); + applyChanges = () => { + const { selectedGroups } = this.state; + const { jobs } = this.props; + const newJobs = jobs.map(j => ({ + id: j.id, + oldGroups: j.groups, + newGroups: [], + })); - for (const gId in selectedGroups) { - if (selectedGroups.hasOwnProperty(gId)) { - const group = selectedGroups[gId]; - newJobs.forEach(j => { - if (group.partial === false || (group.partial === true && j.oldGroups.includes(gId))) { - j.newGroups.push(gId); - } - }); - } + for (const gId in selectedGroups) { + if (selectedGroups.hasOwnProperty(gId)) { + const group = selectedGroups[gId]; + newJobs.forEach(j => { + if (group.partial === false || (group.partial === true && j.oldGroups.includes(gId))) { + j.newGroups.push(gId); + } + }); } + } - const tempJobs = newJobs.map(j => ({ job_id: j.id, groups: j.newGroups })); - ml.jobs - .updateGroups(tempJobs) - .then(resp => { - let success = true; - for (const jobId in resp) { - // check success of each job update - if (resp.hasOwnProperty(jobId)) { - if (resp[jobId].success === false) { - mlMessageBarService.notify.error(resp[jobId].error); - success = false; - } + const tempJobs = newJobs.map(j => ({ job_id: j.id, groups: j.newGroups })); + ml.jobs + .updateGroups(tempJobs) + .then(resp => { + let success = true; + for (const jobId in resp) { + // check success of each job update + if (resp.hasOwnProperty(jobId)) { + if (resp[jobId].success === false) { + mlMessageBarService.notify.error(resp[jobId].error); + success = false; } } + } - if (success) { - // if all are successful refresh the job list - this.refreshJobs(); - this.closePopover(); - } else { - console.error(resp); - } - }) - .catch(error => { - mlMessageBarService.notify.error(error); - console.error(error); - }); + if (success) { + // if all are successful refresh the job list + this.refreshJobs(); + this.closePopover(); + } else { + console.error(resp); + } + }) + .catch(error => { + mlMessageBarService.notify.error(error); + console.error(error); + }); + }; + + addNewGroup = id => { + const newGroup = { + id, + calendarIds: [], + jobIds: [], }; - addNewGroup = id => { - const newGroup = { - id, - calendarIds: [], - jobIds: [], - }; + const groups = this.state.groups; + if (groups.some(g => g.id === newGroup.id) === false) { + groups.push(newGroup); + } - const groups = this.state.groups; - if (groups.some(g => g.id === newGroup.id) === false) { - groups.push(newGroup); - } + this.setState({ + groups, + }); + }; - this.setState({ - groups, - }); - }; + render() { + const { groups, selectedGroups, edited } = this.state; + const button = ( + + } + > + this.togglePopover()} + disabled={this.canUpdateJob === false} + /> + + ); - render() { - const { intl } = this.props; - const { groups, selectedGroups, edited } = this.state; - const button = ( - this.closePopover()} + > +
+ - } - > - this.togglePopover()} - disabled={this.canUpdateJob === false} - /> - - ); + - return ( - this.closePopover()} - > -
- - - - - + - - + + - + - -
- - - - - - - -
+ +
+ + + + + + +
- - ); - } +
+
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js index 291e7d4945197..f92f9c2fa4a3d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js @@ -16,108 +16,110 @@ import { keyCodes, } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { validateGroupNames } from '../../../validate_job'; -export const NewGroupInput = injectI18n( - class NewGroupInput extends Component { - static propTypes = { - addNewGroup: PropTypes.func.isRequired, - allJobIds: PropTypes.array.isRequired, - }; +export class NewGroupInput extends Component { + static propTypes = { + addNewGroup: PropTypes.func.isRequired, + allJobIds: PropTypes.array.isRequired, + }; - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - tempNewGroupName: '', - groupsValidationError: '', - }; - } + this.state = { + tempNewGroupName: '', + groupsValidationError: '', + }; + } - changeTempNewGroup = e => { - const tempNewGroupName = e.target.value; - let groupsValidationError = ''; + changeTempNewGroup = e => { + const tempNewGroupName = e.target.value; + let groupsValidationError = ''; - if (tempNewGroupName === '') { - groupsValidationError = ''; - } else if (this.props.allJobIds.includes(tempNewGroupName)) { - groupsValidationError = this.props.intl.formatMessage({ - id: - 'xpack.ml.jobsList.multiJobActions.groupSelector.groupsAndJobsCanNotUseSameIdErrorMessage', + if (tempNewGroupName === '') { + groupsValidationError = ''; + } else if (this.props.allJobIds.includes(tempNewGroupName)) { + groupsValidationError = i18n.translate( + 'xpack.ml.jobsList.multiJobActions.groupSelector.groupsAndJobsCanNotUseSameIdErrorMessage', + { defaultMessage: 'A job with this ID already exists. Groups and jobs cannot use the same ID.', - }); - } else { - groupsValidationError = validateGroupNames([tempNewGroupName]).message; - } + } + ); + } else { + groupsValidationError = validateGroupNames([tempNewGroupName]).message; + } - this.setState({ - tempNewGroupName, - groupsValidationError, - }); - }; + this.setState({ + tempNewGroupName, + groupsValidationError, + }); + }; - newGroupKeyPress = e => { - if ( - e.keyCode === keyCodes.ENTER && - this.state.groupsValidationError === '' && - this.state.tempNewGroupName !== '' - ) { - this.addNewGroup(); - } - }; + newGroupKeyPress = e => { + if ( + e.keyCode === keyCodes.ENTER && + this.state.groupsValidationError === '' && + this.state.tempNewGroupName !== '' + ) { + this.addNewGroup(); + } + }; - addNewGroup = () => { - this.props.addNewGroup(this.state.tempNewGroupName); - this.setState({ tempNewGroupName: '' }); - }; + addNewGroup = () => { + this.props.addNewGroup(this.state.tempNewGroupName); + this.setState({ tempNewGroupName: '' }); + }; - render() { - const { intl } = this.props; - const { tempNewGroupName, groupsValidationError } = this.state; + render() { + const { tempNewGroupName, groupsValidationError } = this.state; - return ( -
- - - + + + + - - - - - - + + + + + - - - -
- ); - } + } + )} + disabled={tempNewGroupName === '' || groupsValidationError !== ''} + /> + + + +
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 57953d99a9f20..2739f32aa1055 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -5,13 +5,12 @@ */ import { each } from 'lodash'; -import { toastNotifications } from 'ui/notify'; import { mlMessageBarService } from '../../../components/messagebar'; import rison from 'rison-node'; -import chrome from 'ui/chrome'; import { mlJobService } from '../../../services/job_service'; import { ml } from '../../../services/ml_api_service'; +import { getToastNotifications, getBasePath } from '../../../util/dependency_cache'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { i18n } from '@kbn/i18n'; @@ -58,6 +57,7 @@ export function forceStartDatafeeds(jobs, start, end, finish = () => {}) { }) .catch(error => { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.startJobErrorMessage', { defaultMessage: 'Jobs failed to start', @@ -78,6 +78,7 @@ export function stopDatafeeds(jobs, finish = () => {}) { }) .catch(error => { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.stopJobErrorMessage', { defaultMessage: 'Jobs failed to stop', @@ -139,6 +140,7 @@ function showResults(resp, action) { }); } + const toastNotifications = getToastNotifications(); toastNotifications.addSuccess( i18n.translate('xpack.ml.jobsList.actionExecuteSuccessfullyNotificationMessage', { defaultMessage: @@ -213,6 +215,7 @@ export async function cloneJob(jobId) { window.location.href = '#/jobs/new_job'; } catch (error) { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.cloneJobErrorMessage', { defaultMessage: 'Could not clone {jobId}. Job could not be found', @@ -232,6 +235,7 @@ export function closeJobs(jobs, finish = () => {}) { }) .catch(error => { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.closeJobErrorMessage', { defaultMessage: 'Jobs failed to close', @@ -252,6 +256,7 @@ export function deleteJobs(jobs, finish = () => {}) { }) .catch(error => { mlMessageBarService.notify.error(error); + const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.deleteJobErrorMessage', { defaultMessage: 'Jobs failed to delete', @@ -367,8 +372,9 @@ export function getJobIdUrl(jobId) { }; const encoded = rison.encode(settings); const url = `?mlManagement=${encoded}`; + const basePath = getBasePath(); - return `${chrome.getBasePath()}/app/ml#/jobs${url}`; + return `${basePath.get()}/app/ml#/jobs${url}`; } function getUrlVars(url) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/time_range_picker.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/time_range_picker.tsx index 8c648696a9a7a..212c5ad6ebb31 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/time_range_picker.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/time_range_picker.tsx @@ -8,7 +8,7 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import moment, { Moment } from 'moment'; import { i18n } from '@kbn/i18n'; import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; const WIDTH = '512px'; @@ -23,8 +23,8 @@ interface Props { } export const TimeRangePicker: FC = ({ setTimeRange, timeRange }) => { - const kibanaContext = useKibanaContext(); - const dateFormat: string = kibanaContext.kibanaConfig.get('dateFormat'); + const mlContext = useMlContext(); + const dateFormat: string = mlContext.kibanaConfig.get('dateFormat'); const [startMoment, setStartMoment] = useState(moment(timeRange.start)); const [endMoment, setEndMoment] = useState(moment(timeRange.end)); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx index ed4f7729ccb26..3070fc0afdc33 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; import { LineSeries, ScaleType, CurveType } from '@elastic/charts'; -import { seriesStyle, LINE_COLOR } from '../common/settings'; +import { seriesStyle, useChartColors } from '../common/settings'; interface Props { chartData: any[]; @@ -19,6 +19,7 @@ const lineSeriesStyle = { }; export const Line: FC = ({ chartData }) => { + const { LINE_COLOR } = useChartColors(); return ( = ({ modelData }) => { + const { MODEL_COLOR } = useChartColors(); const model = modelData === undefined ? [] : modelData; return ( = ({ chartData }) => { + const { LINE_COLOR } = useChartColors(); return ( = ({ loading = false, fadeChart, }) => { + const { EVENT_RATE_COLOR_WITH_ANOMALIES, EVENT_RATE_COLOR } = useChartColors(); const barColor = fadeChart ? EVENT_RATE_COLOR_WITH_ANOMALIES : EVENT_RATE_COLOR; return ( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx index 828c91052b30b..b67bab89ce881 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx @@ -8,11 +8,14 @@ import React, { memo, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; - -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-calendars.html`; +import { useMlKibana } from '../../../../../../../../../contexts/kibana'; export const Description: FC = memo(({ children }) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-calendars.html`; const title = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.additionalSection.calendarsSelection.title', { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx index 566bd313dbc6e..cea5f8b1ec813 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx @@ -8,11 +8,14 @@ import React, { memo, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; - -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-configuring-url.html`; +import { useMlKibana } from '../../../../../../../../../contexts/kibana'; export const Description: FC = memo(({ children }) => { + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-url.html`; const title = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.additionalSection.customUrls.title', { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index 4a1626ffcef89..5064ba9df9bee 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -14,7 +14,7 @@ import { isAdvancedJobCreator, } from '../../../../../common/job_creator'; import { ml, BucketSpanEstimatorData } from '../../../../../../../services/ml_api_service'; -import { useKibanaContext } from '../../../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../../../contexts/ml'; import { mlMessageBarService } from '../../../../../../../components/messagebar'; export enum ESTIMATE_STATUS { @@ -24,7 +24,7 @@ export enum ESTIMATE_STATUS { export function useEstimateBucketSpan() { const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const [status, setStatus] = useState(ESTIMATE_STATUS.NOT_RUNNING); @@ -35,10 +35,10 @@ export function useEstimateBucketSpan() { end: jobCreator.end, }, fields: jobCreator.fields.map(f => (f.id === EVENT_RATE_FIELD_ID ? null : f.id)), - index: kibanaContext.currentIndexPattern.title, - query: kibanaContext.combinedQuery, + index: mlContext.currentIndexPattern.title, + query: mlContext.combinedQuery, splitField: undefined, - timeField: kibanaContext.currentIndexPattern.timeFieldName, + timeField: mlContext.currentIndexPattern.timeFieldName, }; if ( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx index ebe113a1f8bef..82524b84d9849 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx @@ -17,12 +17,12 @@ import { } from '../../../../../common/job_creator'; import { getNewJobDefaults } from '../../../../../../../services/ml_server_info'; import { ListItems, falseLabel, trueLabel, defaultLabel, Italic } from '../common'; -import { useKibanaContext } from '../../../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../../../contexts/ml'; export const JobDetails: FC = () => { const { jobCreator } = useContext(JobCreatorContext); - const kibanaContext = useKibanaContext(); - const dateFormat: string = kibanaContext.kibanaConfig.get('dateFormat'); + const mlContext = useMlContext(); + const dateFormat: string = mlContext.kibanaConfig.get('dateFormat'); const { anomaly_detectors: anomalyDetectors } = getNewJobDefaults(); const isAdvanced = isAdvancedJobCreator(jobCreator); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx index de019cbe86f9d..c24c018f50d75 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx @@ -5,11 +5,11 @@ */ import React, { FC, Fragment, useContext, useState } from 'react'; -import { toastNotifications } from 'ui/notify'; import { EuiButton, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { JobRunner } from '../../../../../common/job_runner'; +import { useMlKibana } from '../../../../../../../contexts/kibana'; // @ts-ignore import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout/index'; @@ -23,6 +23,9 @@ interface Props { type ShowFlyout = (jobId: string) => void; export const PostSaveOptions: FC = ({ jobRunner }) => { + const { + services: { notifications }, + } = useMlKibana(); const { jobCreator } = useContext(JobCreatorContext); const [datafeedState, setDatafeedState] = useState(DATAFEED_STATE.STOPPED); const [watchFlyoutVisible, setWatchFlyoutVisible] = useState(false); @@ -42,12 +45,13 @@ export const PostSaveOptions: FC = ({ jobRunner }) => { } async function startJobInRealTime() { + const { toasts } = notifications; setDatafeedState(DATAFEED_STATE.STARTING); if (jobRunner !== null) { try { const started = await jobRunner.startDatafeedInRealTime(true); setDatafeedState(started === true ? DATAFEED_STATE.STARTED : DATAFEED_STATE.STOPPED); - toastNotifications.addSuccess({ + toasts.addSuccess({ title: i18n.translate( 'xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeSuccess', { @@ -58,7 +62,7 @@ export const PostSaveOptions: FC = ({ jobRunner }) => { }); } catch (error) { setDatafeedState(DATAFEED_STATE.STOPPED); - toastNotifications.addDanger({ + toasts.addDanger({ title: i18n.translate( 'xpack.ml.newJob.wizard.summaryStep.postSaveOptions.startJobInRealTimeError', { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 994847864d6bb..75994b5358899 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { PreviousButton } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -38,6 +38,9 @@ import { import { JobSectionTitle, DatafeedSectionTitle } from './components/common'; export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => { + const { + services: { notifications }, + } = useMlKibana(); const { jobCreator, jobValidator, jobValidatorUpdated, resultsLoader } = useContext( JobCreatorContext ); @@ -67,7 +70,8 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => setJobRunner(jr); } catch (error) { // catch and display all job creation errors - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.newJob.wizard.summaryStep.createJobError', { defaultMessage: `Job creation error`, }), @@ -85,7 +89,8 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => advancedStartDatafeed(jobCreator); } catch (error) { // catch and display all job creation errors - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.newJob.wizard.summaryStep.createJobError', { defaultMessage: `Job creation error`, }), diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx index 70a529b8e24d0..f0c5c3ba272c4 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx @@ -6,24 +6,24 @@ import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { timefilter } from 'ui/timefilter'; import moment from 'moment'; import { WizardNav } from '../wizard_nav'; import { StepProps, WIZARD_STEPS } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; -import { useKibanaContext } from '../../../../../contexts/kibana'; +import { useMlContext } from '../../../../../contexts/ml'; import { FullTimeRangeSelector } from '../../../../../components/full_time_range_selector'; import { EventRateChart } from '../charts/event_rate_chart'; import { LineChartPoint } from '../../../common/chart_loader'; import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { GetTimeFieldRangeResponse } from '../../../../../services/ml_api_service'; import { TimeRangePicker, TimeRange } from '../../../common/components'; +import { useMlKibana } from '../../../../../contexts/kibana'; export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) => { - const kibanaContext = useKibanaContext(); + const { services } = useMlKibana(); + const mlContext = useMlContext(); const { jobCreator, @@ -63,6 +63,7 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) max: moment(end), }); // update the timefilter, to keep the URL in sync + const { timefilter } = services.data.query.timefilter; timefilter.setTime({ from: moment(start).toISOString(), to: moment(end).toISOString(), @@ -86,7 +87,8 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) end: range.end.epoch, }); } else { - toastNotifications.addDanger( + const { toasts } = services.notifications; + toasts.addDanger( i18n.translate('xpack.ml.newJob.wizard.timeRangeStep.fullTimeRangeError', { defaultMessage: 'An error occurred obtaining the time range for the index', }) @@ -104,8 +106,8 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx index 2fbedc1cd39bb..9bb9376f3ea14 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx @@ -15,8 +15,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { npStart } from 'ui/new_platform'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; +import { useMlKibana } from '../../../../contexts/kibana'; export interface PageProps { nextStepPath: string; @@ -24,6 +24,7 @@ export interface PageProps { export const Page: FC = ({ nextStepPath }) => { const RESULTS_PER_PAGE = 20; + const { uiSettings, savedObjects } = useMlKibana().services; const onObjectSelection = (id: string, type: string) => { window.location.href = `${nextStepPath}?${ @@ -77,8 +78,8 @@ export const Page: FC = ({ nextStepPath }) => { }, ]} fixedPageSize={RESULTS_PER_PAGE} - uiSettings={npStart.core.uiSettings} - savedObjects={npStart.core.savedObjects} + uiSettings={uiSettings} + savedObjects={savedObjects} /> diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index b1382aef86d30..562ef780bd17b 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -18,7 +18,7 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed'; @@ -27,10 +27,10 @@ import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { CategorizationIcon } from './categorization_job_icon'; export const Page: FC = () => { - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); - const { currentSavedSearch, currentIndexPattern } = kibanaContext; + const { currentSavedSearch, currentIndexPattern } = mlContext; const isTimeBasedIndex = timeBasedIndexCheck(currentIndexPattern); const indexWarningTitle = diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index bc269b22df880..b2383b6c08a58 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -14,12 +14,12 @@ import { EuiTitle, EuiPageContentBody, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Wizard } from './wizard'; import { WIZARD_STEPS } from '../components/step_types'; import { getJobCreatorTitle } from '../../common/job_creator/util/general'; +import { useMlKibana } from '../../../../contexts/kibana'; import { jobCreatorFactory, isAdvancedJobCreator, @@ -33,7 +33,7 @@ import { import { ChartLoader } from '../../common/chart_loader'; import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; import { TimeBuckets } from '../../../../util/time_buckets'; import { ExistingJobsAndGroups, mlJobService } from '../../../../services/job_service'; @@ -52,11 +52,14 @@ export interface PageProps { } export const Page: FC = ({ existingJobsAndGroups, jobType }) => { - const kibanaContext = useKibanaContext(); + const { + services: { notifications }, + } = useMlKibana(); + const mlContext = useMlContext(); const jobCreator = jobCreatorFactory(jobType)( - kibanaContext.currentIndexPattern, - kibanaContext.currentSavedSearch, - kibanaContext.combinedQuery + mlContext.currentIndexPattern, + mlContext.currentSavedSearch, + mlContext.combinedQuery ); const { from, to } = getTimeFilterRange(); @@ -124,7 +127,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { jobCreator.modelPlot = true; } - if (kibanaContext.currentSavedSearch !== null) { + if (mlContext.currentSavedSearch !== null) { // Jobs created from saved searches cannot be cloned in the wizard as the // ML job config holds no reference to the saved search ID. jobCreator.createdBy = null; @@ -147,7 +150,8 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { try { jobCreator.autoSetTimeRange(); } catch (error) { - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.newJob.wizard.autoSetJobCreatorTimeRange.error', { defaultMessage: `Error retrieving beginning and end times of index`, }), @@ -175,10 +179,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { chartInterval.setMaxBars(MAX_BARS); chartInterval.setInterval('auto'); - const chartLoader = new ChartLoader( - kibanaContext.currentIndexPattern, - kibanaContext.combinedQuery - ); + const chartLoader = new ChartLoader(mlContext.currentIndexPattern, mlContext.combinedQuery); const jobValidator = new JobValidator(jobCreator, existingJobsAndGroups); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx index cd3d887c906af..56a787d0d7054 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx @@ -19,7 +19,7 @@ import { JobDetailsStep } from '../components/job_details_step'; import { ValidationStep } from '../components/validation_step'; import { SummaryStep } from '../components/summary_step'; import { DatafeedStep } from '../components/datafeed_step'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; interface Props { currentStep: WIZARD_STEPS; @@ -27,24 +27,24 @@ interface Props { } export const WizardSteps: FC = ({ currentStep, setCurrentStep }) => { - const kibanaContext = useKibanaContext(); + const mlContext = useMlContext(); // store whether the advanced and additional sections have been expanded. // has to be stored at this level to ensure it's remembered on wizard step change const [advancedExpanded, setAdvancedExpanded] = useState(false); const [additionalExpanded, setAdditionalExpanded] = useState(false); function getSummaryStepTitle() { - if (kibanaContext.currentSavedSearch !== null) { + if (mlContext.currentSavedSearch !== null) { return i18n.translate('xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleSavedSearch', { defaultMessage: 'New job from saved search {title}', - values: { title: kibanaContext.currentSavedSearch.attributes.title as string }, + values: { title: mlContext.currentSavedSearch.attributes.title as string }, }); - } else if (kibanaContext.currentIndexPattern.id !== undefined) { + } else if (mlContext.currentIndexPattern.id !== undefined) { return i18n.translate( 'xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleIndexPattern', { defaultMessage: 'New job from index pattern {title}', - values: { title: kibanaContext.currentIndexPattern.title }, + values: { title: mlContext.currentIndexPattern.title }, } ); } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx index 4046bd8b09afa..9d9e8388c3393 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { ModuleJobUI, SAVE_STATE } from '../page'; import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; -import { useKibanaContext } from '../../../../contexts/kibana'; +import { useMlContext } from '../../../../contexts/ml'; import { composeValidators, maxLengthValidator, @@ -52,7 +52,7 @@ export const JobSettingsForm: FC = ({ jobs, }) => { const { from, to } = getTimeFilterRange(); - const { currentIndexPattern: indexPattern } = useKibanaContext(); + const { currentIndexPattern: indexPattern } = useMlContext(); const jobPrefixValidator = composeValidators( patternValidator(/^([a-z0-9]+[a-z0-9\-_]*)?$/), diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index c4a96d9e373c8..8571ae43da587 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -20,10 +20,10 @@ import { EuiCallOut, EuiPanel, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { merge } from 'lodash'; +import { useMlKibana } from '../../../contexts/kibana'; import { ml } from '../../../services/ml_api_service'; -import { useKibanaContext } from '../../../contexts/kibana'; +import { useMlContext } from '../../../contexts/ml'; import { DatafeedResponse, DataRecognizerConfigResponse, @@ -70,6 +70,9 @@ export enum SAVE_STATE { } export const Page: FC = ({ moduleId, existingGroupIds }) => { + const { + services: { notifications }, + } = useMlKibana(); // #region State const [jobPrefix, setJobPrefix] = useState(''); const [jobs, setJobs] = useState([]); @@ -84,7 +87,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { currentSavedSearch: savedSearch, currentIndexPattern: indexPattern, combinedQuery, - } = useKibanaContext(); + } = useMlContext(); const pageTitle = savedSearch !== null ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', { @@ -206,7 +209,8 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { setSaveState(SAVE_STATE.FAILED); // eslint-disable-next-line no-console console.error('Error setting up module', e); - toastNotifications.addDanger({ + const { toasts } = notifications; + toasts.addDanger({ title: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningTitle', { defaultMessage: 'Error setting up module {moduleId}', values: { moduleId }, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts index cb44210b970e7..fa0ed34dca622 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications, getSavedObjectsClient } from '../../../util/dependency_cache'; import { mlJobService } from '../../../services/job_service'; import { ml } from '../../../services/ml_api_service'; import { KibanaObjects } from './page'; @@ -36,6 +35,7 @@ export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): .catch((err: Error) => { // eslint-disable-next-line no-console console.error(`Error checking whether jobs in module ${moduleId} exists`, err); + const toastNotifications = getToastNotifications(); toastNotifications.addWarning({ title: i18n.translate('xpack.ml.newJob.recognize.moduleCheckJobsExistWarningTitle', { defaultMessage: 'Error checking module {moduleId}', @@ -57,7 +57,7 @@ export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): * Gets kibana objects with an existence check. */ export const checkForSavedObjects = async (objects: KibanaObjects): Promise => { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); try { return await Object.keys(objects).reduce(async (prevPromise, type) => { const acc = await prevPromise; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 0f19451b23263..835232a030383 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IUiSettingsClient } from 'src/core/public'; import { esQuery, Query, esKuery } from '../../../../../../../../../src/plugins/data/public'; import { IIndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns'; -import { KibanaConfigTypeFix } from '../../../contexts/kibana'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; import { SavedSearchSavedObject } from '../../../../../common/types/kibana'; import { getQueryFromSavedSearch } from '../../../util/index_utils'; @@ -14,7 +14,7 @@ import { getQueryFromSavedSearch } from '../../../util/index_utils'; // Provider for creating the items used for searching and job creation. export function createSearchItems( - kibanaConfig: KibanaConfigTypeFix, + kibanaConfig: IUiSettingsClient, indexPattern: IIndexPattern, savedSearch: SavedSearchSavedObject | null ) { diff --git a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx index c184a4d4e94e0..96e6aab377962 100644 --- a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx +++ b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx @@ -5,13 +5,13 @@ */ import React from 'react'; -// @ts-ignore No declaration file for module -import { banners } from 'ui/notify'; import { EuiCallOut } from '@elastic/eui'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; // @ts-ignore No declaration file for module import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; import { LICENSE_TYPE } from '../../../common/constants/license'; import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; +import { getOverlays } from '../util/dependency_cache'; let licenseHasExpired = true; let licenseType: LICENSE_TYPE | null = null; @@ -75,9 +75,10 @@ function setLicenseExpired(features: any) { const message = features.message; if (expiredLicenseBannerId === undefined) { // Only show the banner once with no way to dismiss it - expiredLicenseBannerId = banners.add({ - component: , - }); + const overlays = getOverlays(); + expiredLicenseBannerId = overlays.banners.add( + toMountPoint() + ); } } } diff --git a/x-pack/legacy/plugins/ml/public/application/management/index.ts b/x-pack/legacy/plugins/ml/public/application/management/index.ts index 092639cd5fbab..a05de8b0d0880 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/management/index.ts @@ -12,16 +12,35 @@ import { management } from 'ui/management'; import { i18n } from '@kbn/i18n'; +import chrome from 'ui/chrome'; +import { metadata } from 'ui/metadata'; // @ts-ignore No declaration file for module import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; import { JOBS_LIST_PATH } from './management_urls'; import { LICENSE_TYPE } from '../../../common/constants/license'; +import { setDependencyCache } from '../util/dependency_cache'; import './jobs_list'; if ( xpackInfo.get('features.ml.showLinks', false) === true && xpackInfo.get('features.ml.licenseType') === LICENSE_TYPE.FULL ) { + const legacyBasePath = { + prepend: chrome.addBasePath, + get: chrome.getBasePath, + remove: () => {}, + }; + const legacyDocLinks = { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: metadata.branch, + }; + + setDependencyCache({ + docLinks: legacyDocLinks as any, + basePath: legacyBasePath as any, + XSRF: chrome.getXsrfToken(), + }); + management.register('ml', { display: i18n.translate('xpack.ml.management.mlTitle', { defaultMessage: 'Machine Learning', diff --git a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 1591dbcbad6bf..a987ed7feeee9 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -18,7 +18,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { getDocLinks } from '../../../../util/dependency_cache'; // @ts-ignore undeclared module import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index'; @@ -66,12 +66,12 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] { } export const JobsListPage: FC = ({ isMlEnabledInSpace }) => { + const docLinks = getDocLinks(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const tabs = getTabs(isMlEnabledInSpace); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); - - // metadata.branch corresponds to the version used in documentation links. - const anomalyDetectionJobsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-jobs.html`; - const anomalyJobsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-dfanalytics.html`; + const anomalyDetectionJobsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-jobs.html`; + const anomalyJobsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`; const anomalyDetectionDocsLabel = i18n.translate( 'xpack.ml.management.jobsList.anomalyDetectionDocsLabel', diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 1f9d0413d45f9..cda03b21b0d65 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { toastNotifications } from 'ui/notify'; +import { useMlKibana } from '../../../contexts/kibana'; import { AnomalyDetectionTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils'; @@ -55,6 +55,9 @@ interface Props { } export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { + const { + services: { notifications }, + } = useMlKibana(); const [isLoading, setIsLoading] = useState(false); const [groups, setGroups] = useState({}); const [groupsCount, setGroupsCount] = useState(0); @@ -114,7 +117,8 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { setGroups(tempGroups); } catch (e) { - toastNotifications.addDanger( + const { toasts } = notifications; + toasts.addDanger( i18n.translate( 'xpack.ml.overview.anomalyDetection.errorWithFetchingAnomalyScoreNotificationErrorMessage', { diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx index 8648bd211715e..219c195bab111 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx @@ -7,14 +7,10 @@ import React, { FC } from 'react'; import { EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { metadata } from 'ui/metadata'; +import { useMlKibana } from '../../contexts/kibana'; const createJobLink = '#/jobs/new_job/step/index_or_search'; -// metadata.branch corresponds to the version used in documentation links. -const docsLink = `https://www.elastic.co/guide/en/kibana/${metadata.branch}/xpack-ml.html`; const feedbackLink = 'https://www.elastic.co/community/'; -const transformsLink = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/transform`; const whatIsMachineLearningLink = 'https://www.elastic.co/what-is/elasticsearch-machine-learning'; interface Props { @@ -37,70 +33,83 @@ function getCreateJobLink(createAnomalyDetectionJobDisabled: boolean) { ); } -export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }) => ( - - -

- -

-
- - -

- - - - ), - createJob: getCreateJobLink(createAnomalyDetectionJobDisabled), - transforms: ( - - - - ), - whatIsMachineLearning: ( - - - - ), - }} - /> -

-

- -

-

- - - - ), - }} - /> -

-
-
-); +export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }) => { + const { + services: { + docLinks, + http: { basePath }, + }, + } = useMlKibana(); + + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const docsLink = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-ml.html`; + const transformsLink = `${basePath.get()}/app/kibana#/management/elasticsearch/transform`; + + return ( + + +

+ +

+
+ + +

+ + + + ), + createJob: getCreateJobLink(createAnomalyDetectionJobDisabled), + transforms: ( + + + + ), + whatIsMachineLearning: ( + + + + ), + }} + /> +

+

+ +

+

+ + + + ), + }} + /> +

+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts index 30c5fbc497afe..5fc1ea533e87f 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts @@ -9,7 +9,8 @@ import { checkFullLicense } from '../license/check_license'; import { checkGetJobsPrivilege } from '../privilege/check_privilege'; import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; -import { PageDependencies } from './router'; + +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; export interface Resolvers { [name: string]: () => Promise; @@ -17,11 +18,16 @@ export interface Resolvers { export interface ResolverResults { [name: string]: any; } -export const basicResolvers = (deps: PageDependencies): Resolvers => ({ + +interface BasicResolverDependencies { + indexPatterns: IndexPatternsContract; +} + +export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Resolvers => ({ checkFullLicense, getMlNodeCount, loadMlServerInfo, - loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + loadIndexPatterns: () => loadIndexPatterns(indexPatterns), checkGetJobsPrivilege, loadSavedSearches, }); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/router.tsx b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx index 174c1ef1d4fe8..6b56bc154e801 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx @@ -7,11 +7,11 @@ import React, { FC } from 'react'; import { HashRouter, Route, RouteProps } from 'react-router-dom'; import { Location } from 'history'; -import { I18nContext } from 'ui/i18n'; -import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; -import { KibanaContext, KibanaConfigTypeFix, KibanaContextValue } from '../contexts/kibana'; -import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { IUiSettingsClient, ChromeStart } from 'src/core/public'; +import { ChromeBreadcrumb } from 'kibana/public'; +import { IndexPatternsContract } from 'src/plugins/data/public'; +import { MlContext, MlContextValue } from '../contexts/ml'; import * as routes from './routes'; @@ -22,33 +22,30 @@ interface MlRouteProps extends RouteProps { export interface MlRoute { path: string; - render(props: MlRouteProps, config: KibanaConfigTypeFix, deps: PageDependencies): JSX.Element; + render(props: MlRouteProps, deps: PageDependencies): JSX.Element; breadcrumbs: ChromeBreadcrumb[]; } export interface PageProps { location: Location; - config: KibanaConfigTypeFix; deps: PageDependencies; } -export interface PageDependencies { +interface PageDependencies { + setBreadcrumbs: ChromeStart['setBreadcrumbs']; indexPatterns: IndexPatternsContract; + config: IUiSettingsClient; } -export const PageLoader: FC<{ context: KibanaContextValue }> = ({ context, children }) => { +export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => { return context === null ? null : ( - - {children} - + {children} ); }; -export const MlRouter: FC<{ - config: KibanaConfigTypeFix; - setBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => void; - indexPatterns: IndexPatternsContract; -}> = ({ config, setBreadcrumbs, indexPatterns }) => { +export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => { + const setBreadcrumbs = pageDeps.setBreadcrumbs; + return (
@@ -61,7 +58,7 @@ export const MlRouter: FC<{ window.setTimeout(() => { setBreadcrumbs(route.breadcrumbs); }); - return route.render(props, config, { indexPatterns }); + return route.render(props, pageDeps); }} /> ))} diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx index 3a2f445ac6b82..bd7fc434b36ac 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx @@ -21,12 +21,12 @@ const breadcrumbs = [ export const accessDeniedRoute: MlRoute = { path: '/access-denied', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, {}); +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, {}); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 41c286c54836c..3ca23998d5b75 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -30,12 +30,12 @@ const breadcrumbs = [ export const analyticsJobExplorationRoute: MlRoute = { path: '/data_frame_analytics/exploration', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { - const { context } = useResolver('', undefined, config, basicResolvers(deps)); +const PageWrapper: FC = ({ location, deps }) => { + const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); const { _g } = queryString.parse(location.search); let globalState: any = null; try { diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index 31bd10f2138ad..f6d7d91884646 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -25,12 +25,12 @@ const breadcrumbs = [ export const analyticsJobsListRoute: MlRoute = { path: '/data_frame_analytics', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { - const { context } = useResolver('', undefined, config, basicResolvers(deps)); +const PageWrapper: FC = ({ location, deps }) => { + const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index 3faca285319d5..e89834018f5e6 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -23,12 +23,12 @@ const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; export const selectorRoute: MlRoute = { path: '/datavisualizer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ location, deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, checkFindFileStructurePrivilege, }); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 11e6b85f939d3..b4ccccd0776eb 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -36,12 +36,12 @@ const breadcrumbs = [ export const fileBasedRoute: MlRoute = { path: '/filedatavisualizer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { - const { context } = useResolver('', undefined, config, { +const PageWrapper: FC = ({ location, deps }) => { + const { context } = useResolver('', undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), checkFindFileStructurePrivilege, @@ -49,7 +49,7 @@ const PageWrapper: FC = ({ location, config, deps }) => { }); return ( - + ); }; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index ab359238695d4..fa4745f19e3b4 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -32,13 +32,13 @@ const breadcrumbs = [ export const indexBasedRoute: MlRoute = { path: '/jobs/new_job/datavisualizer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { +const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId } = queryString.parse(location.search); - const { context } = useResolver(index, savedSearchId, config, { + const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), checkGetJobsPrivilege, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx index adef7055f9748..b0046f7b8d699 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx @@ -9,8 +9,6 @@ import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; -import { timefilter } from 'ui/timefilter'; - import { MlJobWithTimeRange } from '../../../../common/types/jobs'; import { MlRoute, PageLoader, PageProps } from '../router'; @@ -31,6 +29,7 @@ import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { useMlKibana } from '../../contexts/kibana'; const breadcrumbs = [ ML_BREADCRUMB, @@ -45,12 +44,12 @@ const breadcrumbs = [ export const explorerRoute: MlRoute = { path: '/explorer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config, deps }) => { - const { context, results } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context, results } = useResolver(undefined, undefined, deps.config, { ...basicResolvers(deps), jobs: mlJobService.loadJobsWrapper, jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()), @@ -71,6 +70,8 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [appState, setAppState] = useUrlState('_a'); const [globalState, setGlobalState] = useUrlState('_g'); const [lastRefresh, setLastRefresh] = useState(0); + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; const { jobIds } = useJobSelection(jobsWithTimeRange, getDateFormatTz()); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx index 3d9a2adedc40d..2f4df2d5a307a 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -30,12 +30,12 @@ const breadcrumbs = [ export const jobListRoute: MlRoute = { path: '/jobs', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config, deps }) => { - const { context } = useResolver(undefined, undefined, config, basicResolvers(deps)); +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); const [globalState, setGlobalState] = useUrlState('_g'); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index b81058a9c89af..ae35d783517d3 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -6,12 +6,11 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { MlRoute, PageLoader, PageDependencies } from '../../router'; +import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; -import { KibanaConfigTypeFix } from '../../../contexts/kibana'; import { checkBasicLicense } from '../../../license/check_license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; @@ -22,6 +21,11 @@ enum MODE { DATAVISUALIZER, } +interface IndexOrSearchPageProps extends PageProps { + nextStepPath: string; + mode: MODE; +} + const breadcrumbs = [ ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, @@ -35,9 +39,9 @@ const breadcrumbs = [ export const indexOrSearchRoute: MlRoute = { path: '/jobs/new_job/step/index_or_search', - render: (props, config, deps) => ( + render: (props, deps) => ( ( + render: (props, deps) => ( = ({ config, nextStepPath, deps, mode }) => { +const PageWrapper: FC = ({ nextStepPath, deps, mode }) => { const newJobResolvers = { ...basicResolvers(deps), preConfiguredJobRedirect: () => preConfiguredJobRedirect(deps.indexPatterns), @@ -79,7 +78,7 @@ const PageWrapper: FC<{ const { context } = useResolver( undefined, undefined, - config, + deps.config, mode === MODE.NEW_JOB ? newJobResolvers : dataVizResolvers ); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index e537a186ec784..c2e87f065116e 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -28,13 +28,13 @@ const breadcrumbs = [ export const jobTypeRoute: MlRoute = { path: '/jobs/new_job/step/job_type', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ location, config, deps }) => { +const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId } = queryString.parse(location.search); - const { context } = useResolver(index, savedSearchId, config, basicResolvers(deps)); + const { context } = useResolver(index, savedSearchId, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 4f5085facfb29..78f72a7b7a39b 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -30,21 +30,19 @@ const breadcrumbs = [ export const recognizeRoute: MlRoute = { path: '/jobs/new_job/recognize', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; export const checkViewOrCreateRoute: MlRoute = { path: '/modules/check_view_or_create', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: [], }; -const PageWrapper: FC = ({ location, config, deps }) => { +const PageWrapper: FC = ({ location, deps }) => { const { id, index, savedSearchId } = queryString.parse(location.search); - const { context, results } = useResolver(index, savedSearchId, config, { + const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), existingJobsAndGroups: mlJobService.getJobAndGroupIds, }); @@ -56,10 +54,10 @@ const PageWrapper: FC = ({ location, config, deps }) => { ); }; -const CheckViewOrCreateWrapper: FC = ({ location, config, deps }) => { +const CheckViewOrCreateWrapper: FC = ({ location, deps }) => { const { id: moduleId, index: indexPatternId } = queryString.parse(location.search); // the single resolver checkViewOrCreateJobs redirects only. so will always reject - useResolver(undefined, undefined, config, { + useResolver(undefined, undefined, deps.config, { checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId), }); return null; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 99c0511cd09ce..230d96456427c 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -84,47 +84,37 @@ const categorizationBreadcrumbs = [ export const singleMetricRoute: MlRoute = { path: '/jobs/new_job/single_metric', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: singleMetricBreadcrumbs, }; export const multiMetricRoute: MlRoute = { path: '/jobs/new_job/multi_metric', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: multiMetricBreadcrumbs, }; export const populationRoute: MlRoute = { path: '/jobs/new_job/population', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: populationBreadcrumbs, }; export const advancedRoute: MlRoute = { path: '/jobs/new_job/advanced', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: advancedBreadcrumbs, }; export const categorizationRoute: MlRoute = { path: '/jobs/new_job/categorization', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: categorizationBreadcrumbs, }; -const PageWrapper: FC = ({ location, config, jobType, deps }) => { +const PageWrapper: FC = ({ location, jobType, deps }) => { const { index, savedSearchId } = queryString.parse(location.search); - const { context, results } = useResolver(index, savedSearchId, config, { + const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), privileges: checkCreateJobsPrivilege, jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx index fe9f4336148f3..85227c11582d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx @@ -30,12 +30,12 @@ const breadcrumbs = [ export const overviewRoute: MlRoute = { path: '/overview', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, getMlNodeCount, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index 56ff57f6610b2..fdbfcb3397c75 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -34,12 +34,12 @@ const breadcrumbs = [ export const calendarListRoute: MlRoute = { path: '/settings/calendars_list', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, getMlNodeCount, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index fb68f103e1b77..7f622a1bba62b 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -54,28 +54,24 @@ const editBreadcrumbs = [ export const newCalendarRoute: MlRoute = { path: '/settings/calendars_list/new_calendar', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: newBreadcrumbs, }; export const editCalendarRoute: MlRoute = { path: '/settings/calendars_list/edit_calendar/:calendarId', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: editBreadcrumbs, }; -const PageWrapper: FC = ({ location, config, mode }) => { +const PageWrapper: FC = ({ location, mode, deps }) => { let calendarId: string | undefined; if (mode === MODE.EDIT) { const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); calendarId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; } - const { context } = useResolver(undefined, undefined, config, { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, checkMlNodesAvailable, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index cb19883e962c1..6a4ce271bff17 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -35,12 +35,12 @@ const breadcrumbs = [ export const filterListRoute: MlRoute = { path: '/settings/filter_lists', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, getMlNodeCount, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 7a596a488ddb6..4fa15ebaac21a 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -54,28 +54,24 @@ const editBreadcrumbs = [ export const newFilterListRoute: MlRoute = { path: '/settings/filter_lists/new_filter_list', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: newBreadcrumbs, }; export const editFilterListRoute: MlRoute = { path: '/settings/filter_lists/edit_filter_list/:filterId', - render: (props, config, deps) => ( - - ), + render: (props, deps) => , breadcrumbs: editBreadcrumbs, }; -const PageWrapper: FC = ({ location, config, mode }) => { +const PageWrapper: FC = ({ location, mode, deps }) => { let filterId: string | undefined; if (mode === MODE.EDIT) { const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); filterId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; } - const { context } = useResolver(undefined, undefined, config, { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, checkMlNodesAvailable, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx index b62ecc0539e72..846512503ede5 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -24,12 +24,12 @@ const breadcrumbs = [ML_BREADCRUMB, SETTINGS]; export const settingsRoute: MlRoute = { path: '/settings', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs, }; -const PageWrapper: FC = ({ config }) => { - const { context } = useResolver(undefined, undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, checkGetJobsPrivilege, getMlNodeCount, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index 6917ec718d3a8..0ae42aa44e089 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -12,7 +12,69 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer'; -jest.mock('ui/new_platform'); +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + query: { + timefilter: { + timefilter: { + enableTimeRangeSelector: jest.fn(), + enableAutoRefreshSelector: jest.fn(), + getRefreshInterval: jest.fn(), + setRefreshInterval: jest.fn(), + getTime: jest.fn(), + isAutoRefreshSelectorEnabled: jest.fn(), + isTimeRangeSelectorEnabled: jest.fn(), + getRefreshIntervalUpdate$: jest.fn(), + getTimeUpdate$: jest.fn(), + getEnabledUpdated$: jest.fn(), + }, + history: { get: jest.fn() }, + }, + }, + }, + }, + }, +})); + +jest.mock('../../contexts/kibana', () => ({ + useMlKibana: () => { + return { + services: { + uiSettings: { get: jest.fn() }, + data: { + query: { + timefilter: { + timefilter: { + enableTimeRangeSelector: jest.fn(), + enableAutoRefreshSelector: jest.fn(), + getRefreshInterval: jest.fn(), + setRefreshInterval: jest.fn(), + getTime: jest.fn(), + isAutoRefreshSelectorEnabled: jest.fn(), + isTimeRangeSelectorEnabled: jest.fn(), + getRefreshIntervalUpdate$: jest.fn(), + getTimeUpdate$: jest.fn(), + getEnabledUpdated$: jest.fn(), + }, + history: { get: jest.fn() }, + }, + }, + }, + notifications: { + toasts: { + addDanger: () => {}, + }, + }, + }, + }; + }, +})); + +jest.mock('../../util/dependency_cache', () => ({ + getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }), +})); describe('TimeSeriesExplorerUrlStateManager', () => { test('Initial render shows "No single metric jobs found"', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 4455e6e99ada7..2bf3d50c3678c 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -13,8 +13,6 @@ import queryString from 'query-string'; import { i18n } from '@kbn/i18n'; -import { timefilter } from 'ui/timefilter'; - import { MlJobWithTimeRange } from '../../../../common/types/jobs'; import { TimeSeriesExplorer } from '../../timeseriesexplorer'; @@ -39,10 +37,11 @@ import { useRefresh } from '../use_refresh'; import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { useMlKibana } from '../../contexts/kibana'; export const timeSeriesExplorerRoute: MlRoute = { path: '/timeseriesexplorer', - render: (props, config, deps) => , + render: (props, deps) => , breadcrumbs: [ ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, @@ -55,8 +54,8 @@ export const timeSeriesExplorerRoute: MlRoute = { ], }; -const PageWrapper: FC = ({ config, deps }) => { - const { context, results } = useResolver('', undefined, config, { +const PageWrapper: FC = ({ deps }) => { + const { context, results } = useResolver('', undefined, deps.config, { ...basicResolvers(deps), jobs: mlJobService.loadJobsWrapper, jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()), @@ -65,7 +64,7 @@ const PageWrapper: FC = ({ config, deps }) => { return ( @@ -91,6 +90,8 @@ export const TimeSeriesExplorerUrlStateManager: FC(); + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; const refresh = useRefresh(); useEffect(() => { diff --git a/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts index 3716b9715bb5b..ee4f77767fce8 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts @@ -5,6 +5,7 @@ */ import { useEffect, useState } from 'react'; +import { IUiSettingsClient } from 'src/core/public'; import { getIndexPatternById, getIndexPatternsContract, @@ -12,14 +13,14 @@ import { } from '../util/index_utils'; import { createSearchItems } from '../jobs/new_job/utils/new_job_utils'; import { ResolverResults, Resolvers } from './resolvers'; -import { KibanaConfigTypeFix, KibanaContextValue } from '../contexts/kibana'; +import { MlContextValue } from '../contexts/ml'; export const useResolver = ( indexPatternId: string | undefined, savedSearchId: string | undefined, - config: KibanaConfigTypeFix, + config: IUiSettingsClient, resolvers: Resolvers -): { context: KibanaContextValue; results: ResolverResults } => { +): { context: MlContextValue; results: ResolverResults } => { const funcNames = Object.keys(resolvers); // Object.entries gets this wrong?! const funcs = Object.values(resolvers); // Object.entries gets this wrong?! const tempResults = funcNames.reduce((p, c) => { diff --git a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts index 41200759b7c8a..73a30dbcd71b2 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts @@ -6,24 +6,23 @@ // service for interacting with the server -import chrome from 'ui/chrome'; - -// @ts-ignore -import { addSystemApiHeader } from 'ui/system_api'; import { fromFetch } from 'rxjs/fetch'; import { from, Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; +import { getXSRF } from '../util/dependency_cache'; + export interface HttpOptions { url?: string; } function getResultHeaders(headers: HeadersInit): HeadersInit { - return addSystemApiHeader({ + return { + asSystemRequest: false, 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), + 'kbn-version': getXSRF(), ...headers, - }); + } as HeadersInit; } export function http(options: any) { @@ -31,11 +30,7 @@ export function http(options: any) { if (options && options.url) { let url = ''; url = url + (options.url || ''); - const headers: Record = addSystemApiHeader({ - 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), - ...options.headers, - }); + const headers = getResultHeaders(options.headers ?? {}); const allHeaders = options.headers === undefined ? headers : { ...options.headers, ...headers }; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts index 54d55159646f6..cc30d481a6355 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - import { Annotation } from '../../../../common/types/annotations'; import { http, http$ } from '../http_service'; - -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const annotations = { getAnnotations(obj: { @@ -18,21 +15,21 @@ export const annotations = { latestMs: number; maxAnnotations: number; }) { - return http$<{ annotations: Record }>(`${basePath}/annotations`, { + return http$<{ annotations: Record }>(`${basePath()}/annotations`, { method: 'POST', body: obj, }); }, indexAnnotation(obj: any) { return http({ - url: `${basePath}/annotations/index`, + url: `${basePath()}/annotations/index`, method: 'PUT', data: obj, }); }, deleteAnnotation(id: string) { return http({ - url: `${basePath}/annotations/delete/${id}`, + url: `${basePath()}/annotations/delete/${id}`, method: 'DELETE', }); }, diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js index 6ff0b45454abf..8a74cddce3f6d 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js @@ -4,75 +4,73 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - import { http } from '../http_service'; -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const dataFrameAnalytics = { getDataFrameAnalytics(analyticsId) { const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; return http({ - url: `${basePath}/data_frame/analytics${analyticsIdString}`, + url: `${basePath()}/data_frame/analytics${analyticsIdString}`, method: 'GET', }); }, getDataFrameAnalyticsStats(analyticsId) { if (analyticsId !== undefined) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}/_stats`, + url: `${basePath()}/data_frame/analytics/${analyticsId}/_stats`, method: 'GET', }); } return http({ - url: `${basePath}/data_frame/analytics/_stats`, + url: `${basePath()}/data_frame/analytics/_stats`, method: 'GET', }); }, createDataFrameAnalytics(analyticsId, analyticsConfig) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}`, + url: `${basePath()}/data_frame/analytics/${analyticsId}`, method: 'PUT', data: analyticsConfig, }); }, evaluateDataFrameAnalytics(evaluateConfig) { return http({ - url: `${basePath}/data_frame/_evaluate`, + url: `${basePath()}/data_frame/_evaluate`, method: 'POST', data: evaluateConfig, }); }, explainDataFrameAnalytics(jobConfig) { return http({ - url: `${basePath}/data_frame/analytics/_explain`, + url: `${basePath()}/data_frame/analytics/_explain`, method: 'POST', data: jobConfig, }); }, deleteDataFrameAnalytics(analyticsId) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}`, + url: `${basePath()}/data_frame/analytics/${analyticsId}`, method: 'DELETE', }); }, startDataFrameAnalytics(analyticsId) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}/_start`, + url: `${basePath()}/data_frame/analytics/${analyticsId}/_start`, method: 'POST', }); }, stopDataFrameAnalytics(analyticsId, force = false) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}/_stop?force=${force}`, + url: `${basePath()}/data_frame/analytics/${analyticsId}/_stop?force=${force}`, method: 'POST', }); }, getAnalyticsAuditMessages(analyticsId) { return http({ - url: `${basePath}/data_frame/analytics/${analyticsId}/messages`, + url: `${basePath()}/data_frame/analytics/${analyticsId}/messages`, method: 'GET', }); }, diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js index c9f6bc08e75ec..364fa57ba7d6b 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - import { http } from '../http_service'; -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const fileDatavisualizer = { analyzeFile(obj, params = {}) { @@ -22,7 +20,7 @@ export const fileDatavisualizer = { } } return http({ - url: `${basePath}/file_data_visualizer/analyze_file${paramString}`, + url: `${basePath()}/file_data_visualizer/analyze_file${paramString}`, method: 'POST', data: obj, }); @@ -33,7 +31,7 @@ export const fileDatavisualizer = { const { index, data, settings, mappings, ingestPipeline } = obj; return http({ - url: `${basePath}/file_data_visualizer/import${paramString}`, + url: `${basePath()}/file_data_visualizer/import${paramString}`, method: 'POST', data: { index, diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js index 1377ca7e60261..010a531a192f1 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js @@ -7,31 +7,29 @@ // Service for querying filters, which hold lists of entities, // for example a list of known safe URL domains. -import chrome from 'ui/chrome'; - import { http } from '../http_service'; -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const filters = { filters(obj) { const filterId = obj && obj.filterId ? `/${obj.filterId}` : ''; return http({ - url: `${basePath}/filters${filterId}`, + url: `${basePath()}/filters${filterId}`, method: 'GET', }); }, filtersStats() { return http({ - url: `${basePath}/filters/_stats`, + url: `${basePath()}/filters/_stats`, method: 'GET', }); }, addFilter(filterId, description, items) { return http({ - url: `${basePath}/filters`, + url: `${basePath()}/filters`, method: 'PUT', data: { filterId, @@ -54,7 +52,7 @@ export const filters = { } return http({ - url: `${basePath}/filters/${filterId}`, + url: `${basePath()}/filters/${filterId}`, method: 'PUT', data, }); @@ -62,7 +60,7 @@ export const filters = { deleteFilter(filterId) { return http({ - url: `${basePath}/filters/${filterId}`, + url: `${basePath()}/filters/${filterId}`, method: 'DELETE', }); }, diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts index 6420b60e4c838..6cb8eccafe151 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -29,6 +29,8 @@ import { } from '../../../../common/types/categories'; import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/new_job'; +declare const basePath: () => string; + // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use // TypeScript and rely on the methods typed in here. diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js index 565cf0c0bfa8b..6fdc76d7244d3 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js @@ -5,8 +5,6 @@ */ import { pick } from 'lodash'; -import chrome from 'ui/chrome'; - import { http, http$ } from '../http_service'; import { annotations } from './annotations'; @@ -15,27 +13,30 @@ import { filters } from './filters'; import { results } from './results'; import { jobs } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; +import { getBasePath } from '../../util/dependency_cache'; -const basePath = chrome.addBasePath('/api/ml'); +export function basePath() { + return getBasePath().prepend('/api/ml'); +} export const ml = { getJobs(obj) { const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; return http({ - url: `${basePath}/anomaly_detectors${jobId}`, + url: `${basePath()}/anomaly_detectors${jobId}`, }); }, getJobStats(obj) { const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; return http({ - url: `${basePath}/anomaly_detectors${jobId}/_stats`, + url: `${basePath()}/anomaly_detectors${jobId}/_stats`, }); }, addJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}`, method: 'PUT', data: obj.job, }); @@ -43,35 +44,35 @@ export const ml = { openJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_open`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/_open`, method: 'POST', }); }, closeJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_close`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/_close`, method: 'POST', }); }, deleteJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}`, method: 'DELETE', }); }, forceDeleteJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}?force=true`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}?force=true`, method: 'DELETE', }); }, updateJob(obj) { return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_update`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/_update`, method: 'POST', data: obj.job, }); @@ -79,7 +80,7 @@ export const ml = { estimateBucketSpan(obj) { return http({ - url: `${basePath}/validate/estimate_bucket_span`, + url: `${basePath()}/validate/estimate_bucket_span`, method: 'POST', data: obj, }); @@ -87,14 +88,14 @@ export const ml = { validateJob(obj) { return http({ - url: `${basePath}/validate/job`, + url: `${basePath()}/validate/job`, method: 'POST', data: obj, }); }, validateCardinality$(obj) { - return http$(`${basePath}/validate/cardinality`, { + return http$(`${basePath()}/validate/cardinality`, { method: 'POST', body: obj, }); @@ -103,20 +104,20 @@ export const ml = { getDatafeeds(obj) { const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : ''; return http({ - url: `${basePath}/datafeeds${datafeedId}`, + url: `${basePath()}/datafeeds${datafeedId}`, }); }, getDatafeedStats(obj) { const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : ''; return http({ - url: `${basePath}/datafeeds${datafeedId}/_stats`, + url: `${basePath()}/datafeeds${datafeedId}/_stats`, }); }, addDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}`, + url: `${basePath()}/datafeeds/${obj.datafeedId}`, method: 'PUT', data: obj.datafeedConfig, }); @@ -124,7 +125,7 @@ export const ml = { updateDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_update`, + url: `${basePath()}/datafeeds/${obj.datafeedId}/_update`, method: 'POST', data: obj.datafeedConfig, }); @@ -132,14 +133,14 @@ export const ml = { deleteDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}`, + url: `${basePath()}/datafeeds/${obj.datafeedId}`, method: 'DELETE', }); }, forceDeleteDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}?force=true`, + url: `${basePath()}/datafeeds/${obj.datafeedId}?force=true`, method: 'DELETE', }); }, @@ -153,7 +154,7 @@ export const ml = { data.end = obj.end; } return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_start`, + url: `${basePath()}/datafeeds/${obj.datafeedId}/_start`, method: 'POST', data, }); @@ -161,21 +162,21 @@ export const ml = { stopDatafeed(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_stop`, + url: `${basePath()}/datafeeds/${obj.datafeedId}/_stop`, method: 'POST', }); }, datafeedPreview(obj) { return http({ - url: `${basePath}/datafeeds/${obj.datafeedId}/_preview`, + url: `${basePath()}/datafeeds/${obj.datafeedId}/_preview`, method: 'GET', }); }, validateDetector(obj) { return http({ - url: `${basePath}/anomaly_detectors/_validate/detector`, + url: `${basePath()}/anomaly_detectors/_validate/detector`, method: 'POST', data: obj.detector, }); @@ -188,7 +189,7 @@ export const ml = { } return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/_forecast`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/_forecast`, method: 'POST', data, }); @@ -197,7 +198,7 @@ export const ml = { overallBuckets(obj) { const data = pick(obj, ['topN', 'bucketSpan', 'start', 'end']); return http({ - url: `${basePath}/anomaly_detectors/${obj.jobId}/results/overall_buckets`, + url: `${basePath()}/anomaly_detectors/${obj.jobId}/results/overall_buckets`, method: 'POST', data, }); @@ -205,7 +206,7 @@ export const ml = { hasPrivileges(obj) { return http({ - url: `${basePath}/_has_privileges`, + url: `${basePath()}/_has_privileges`, method: 'POST', data: obj, }); @@ -213,21 +214,21 @@ export const ml = { checkMlPrivileges() { return http({ - url: `${basePath}/ml_capabilities`, + url: `${basePath()}/ml_capabilities`, method: 'GET', }); }, checkManageMLPrivileges() { return http({ - url: `${basePath}/ml_capabilities?ignoreSpaces=true`, + url: `${basePath()}/ml_capabilities?ignoreSpaces=true`, method: 'GET', }); }, getNotificationSettings() { return http({ - url: `${basePath}/notification_settings`, + url: `${basePath()}/notification_settings`, method: 'GET', }); }, @@ -241,7 +242,7 @@ export const ml = { data.fields = obj.fields; } return http({ - url: `${basePath}/indices/field_caps`, + url: `${basePath()}/indices/field_caps`, method: 'POST', data, }); @@ -249,28 +250,28 @@ export const ml = { recognizeIndex(obj) { return http({ - url: `${basePath}/modules/recognize/${obj.indexPatternTitle}`, + url: `${basePath()}/modules/recognize/${obj.indexPatternTitle}`, method: 'GET', }); }, listDataRecognizerModules() { return http({ - url: `${basePath}/modules/get_module`, + url: `${basePath()}/modules/get_module`, method: 'GET', }); }, getDataRecognizerModule(obj) { return http({ - url: `${basePath}/modules/get_module/${obj.moduleId}`, + url: `${basePath()}/modules/get_module/${obj.moduleId}`, method: 'GET', }); }, dataRecognizerModuleJobsExist(obj) { return http({ - url: `${basePath}/modules/jobs_exist/${obj.moduleId}`, + url: `${basePath()}/modules/jobs_exist/${obj.moduleId}`, method: 'GET', }); }, @@ -289,7 +290,7 @@ export const ml = { ]); return http({ - url: `${basePath}/modules/setup/${obj.moduleId}`, + url: `${basePath()}/modules/setup/${obj.moduleId}`, method: 'POST', data, }); @@ -308,7 +309,7 @@ export const ml = { ]); return http({ - url: `${basePath}/data_visualizer/get_field_stats/${obj.indexPatternTitle}`, + url: `${basePath()}/data_visualizer/get_field_stats/${obj.indexPatternTitle}`, method: 'POST', data, }); @@ -326,7 +327,7 @@ export const ml = { ]); return http({ - url: `${basePath}/data_visualizer/get_overall_stats/${obj.indexPatternTitle}`, + url: `${basePath()}/data_visualizer/get_overall_stats/${obj.indexPatternTitle}`, method: 'POST', data, }); @@ -346,14 +347,14 @@ export const ml = { calendarIdsPathComponent = `/${calendarIds.join(',')}`; } return http({ - url: `${basePath}/calendars${calendarIdsPathComponent}`, + url: `${basePath()}/calendars${calendarIdsPathComponent}`, method: 'GET', }); }, addCalendar(obj) { return http({ - url: `${basePath}/calendars`, + url: `${basePath()}/calendars`, method: 'PUT', data: obj, }); @@ -362,7 +363,7 @@ export const ml = { updateCalendar(obj) { const calendarId = obj && obj.calendarId ? `/${obj.calendarId}` : ''; return http({ - url: `${basePath}/calendars${calendarId}`, + url: `${basePath()}/calendars${calendarId}`, method: 'PUT', data: obj, }); @@ -370,21 +371,21 @@ export const ml = { deleteCalendar(obj) { return http({ - url: `${basePath}/calendars/${obj.calendarId}`, + url: `${basePath()}/calendars/${obj.calendarId}`, method: 'DELETE', }); }, mlNodeCount() { return http({ - url: `${basePath}/ml_node_count`, + url: `${basePath()}/ml_node_count`, method: 'GET', }); }, mlInfo() { return http({ - url: `${basePath}/info`, + url: `${basePath()}/info`, method: 'GET', }); }, @@ -402,7 +403,7 @@ export const ml = { ]); return http({ - url: `${basePath}/validate/calculate_model_memory_limit`, + url: `${basePath()}/validate/calculate_model_memory_limit`, method: 'POST', data, }); @@ -419,7 +420,7 @@ export const ml = { ]); return http({ - url: `${basePath}/fields_service/field_cardinality`, + url: `${basePath()}/fields_service/field_cardinality`, method: 'POST', data, }); @@ -429,7 +430,7 @@ export const ml = { const data = pick(obj, ['index', 'timeFieldName', 'query']); return http({ - url: `${basePath}/fields_service/time_field_range`, + url: `${basePath()}/fields_service/time_field_range`, method: 'POST', data, }); @@ -437,21 +438,21 @@ export const ml = { esSearch(obj) { return http({ - url: `${basePath}/es_search`, + url: `${basePath()}/es_search`, method: 'POST', data: obj, }); }, esSearch$(obj) { - return http$(`${basePath}/es_search`, { + return http$(`${basePath()}/es_search`, { method: 'POST', body: obj, }); }, getIndices() { - const tempBasePath = chrome.addBasePath('/api'); + const tempBasePath = getBasePath().prepend('/api'); return http({ url: `${tempBasePath}/index_management/indices`, method: 'GET', diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js index 05d98dc1a1e64..cc9593d946bd1 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js @@ -4,16 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - import { http } from '../http_service'; -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const jobs = { jobsSummary(jobIds) { return http({ - url: `${basePath}/jobs/jobs_summary`, + url: `${basePath()}/jobs/jobs_summary`, method: 'POST', data: { jobIds, @@ -23,7 +21,7 @@ export const jobs = { jobsWithTimerange(dateFormatTz) { return http({ - url: `${basePath}/jobs/jobs_with_timerange`, + url: `${basePath()}/jobs/jobs_with_timerange`, method: 'POST', data: { dateFormatTz, @@ -33,7 +31,7 @@ export const jobs = { jobs(jobIds) { return http({ - url: `${basePath}/jobs/jobs`, + url: `${basePath()}/jobs/jobs`, method: 'POST', data: { jobIds, @@ -43,14 +41,14 @@ export const jobs = { groups() { return http({ - url: `${basePath}/jobs/groups`, + url: `${basePath()}/jobs/groups`, method: 'GET', }); }, updateGroups(updatedJobs) { return http({ - url: `${basePath}/jobs/update_groups`, + url: `${basePath()}/jobs/update_groups`, method: 'POST', data: { jobs: updatedJobs, @@ -60,7 +58,7 @@ export const jobs = { forceStartDatafeeds(datafeedIds, start, end) { return http({ - url: `${basePath}/jobs/force_start_datafeeds`, + url: `${basePath()}/jobs/force_start_datafeeds`, method: 'POST', data: { datafeedIds, @@ -72,7 +70,7 @@ export const jobs = { stopDatafeeds(datafeedIds) { return http({ - url: `${basePath}/jobs/stop_datafeeds`, + url: `${basePath()}/jobs/stop_datafeeds`, method: 'POST', data: { datafeedIds, @@ -82,7 +80,7 @@ export const jobs = { deleteJobs(jobIds) { return http({ - url: `${basePath}/jobs/delete_jobs`, + url: `${basePath()}/jobs/delete_jobs`, method: 'POST', data: { jobIds, @@ -92,7 +90,7 @@ export const jobs = { closeJobs(jobIds) { return http({ - url: `${basePath}/jobs/close_jobs`, + url: `${basePath()}/jobs/close_jobs`, method: 'POST', data: { jobIds, @@ -104,21 +102,21 @@ export const jobs = { const jobIdString = jobId !== undefined ? `/${jobId}` : ''; const fromString = from !== undefined ? `?from=${from}` : ''; return http({ - url: `${basePath}/job_audit_messages/messages${jobIdString}${fromString}`, + url: `${basePath()}/job_audit_messages/messages${jobIdString}${fromString}`, method: 'GET', }); }, deletingJobTasks() { return http({ - url: `${basePath}/jobs/deleting_jobs_tasks`, + url: `${basePath()}/jobs/deleting_jobs_tasks`, method: 'GET', }); }, jobsExist(jobIds) { return http({ - url: `${basePath}/jobs/jobs_exist`, + url: `${basePath()}/jobs/jobs_exist`, method: 'POST', data: { jobIds, @@ -129,7 +127,7 @@ export const jobs = { newJobCaps(indexPatternTitle, isRollup = false) { const isRollupString = isRollup === true ? `?rollup=true` : ''; return http({ - url: `${basePath}/jobs/new_job_caps/${indexPatternTitle}${isRollupString}`, + url: `${basePath()}/jobs/new_job_caps/${indexPatternTitle}${isRollupString}`, method: 'GET', }); }, @@ -146,7 +144,7 @@ export const jobs = { splitFieldValue ) { return http({ - url: `${basePath}/jobs/new_job_line_chart`, + url: `${basePath()}/jobs/new_job_line_chart`, method: 'POST', data: { indexPatternTitle, @@ -173,7 +171,7 @@ export const jobs = { splitFieldName ) { return http({ - url: `${basePath}/jobs/new_job_population_chart`, + url: `${basePath()}/jobs/new_job_population_chart`, method: 'POST', data: { indexPatternTitle, @@ -190,14 +188,14 @@ export const jobs = { getAllJobAndGroupIds() { return http({ - url: `${basePath}/jobs/all_jobs_and_group_ids`, + url: `${basePath()}/jobs/all_jobs_and_group_ids`, method: 'GET', }); }, getLookBackProgress(jobId, start, end) { return http({ - url: `${basePath}/jobs/look_back_progress`, + url: `${basePath()}/jobs/look_back_progress`, method: 'POST', data: { jobId, @@ -218,7 +216,7 @@ export const jobs = { analyzer ) { return http({ - url: `${basePath}/jobs/categorization_field_examples`, + url: `${basePath()}/jobs/categorization_field_examples`, method: 'POST', data: { indexPatternTitle, @@ -235,7 +233,7 @@ export const jobs = { topCategories(jobId, count) { return http({ - url: `${basePath}/jobs/top_categories`, + url: `${basePath()}/jobs/top_categories`, method: 'POST', data: { jobId, diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js index 38ae777106680..e770e80f4c4d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js @@ -6,11 +6,9 @@ // Service for obtaining data for the ML Results dashboards. -import chrome from 'ui/chrome'; - import { http, http$ } from '../http_service'; -const basePath = chrome.addBasePath('/api/ml'); +import { basePath } from './index'; export const results = { getAnomaliesTableData( @@ -26,7 +24,7 @@ export const results = { maxExamples, influencersFilterQuery ) { - return http$(`${basePath}/results/anomalies_table_data`, { + return http$(`${basePath()}/results/anomalies_table_data`, { method: 'POST', body: { jobIds, @@ -46,7 +44,7 @@ export const results = { getMaxAnomalyScore(jobIds, earliestMs, latestMs) { return http({ - url: `${basePath}/results/max_anomaly_score`, + url: `${basePath()}/results/max_anomaly_score`, method: 'POST', data: { jobIds, @@ -58,7 +56,7 @@ export const results = { getCategoryDefinition(jobId, categoryId) { return http({ - url: `${basePath}/results/category_definition`, + url: `${basePath()}/results/category_definition`, method: 'POST', data: { jobId, categoryId }, }); @@ -66,7 +64,7 @@ export const results = { getCategoryExamples(jobId, categoryIds, maxExamples) { return http({ - url: `${basePath}/results/category_examples`, + url: `${basePath()}/results/category_examples`, method: 'POST', data: { jobId, @@ -77,7 +75,7 @@ export const results = { }, fetchPartitionFieldsValues(jobId, searchTerm, criteriaFields, earliestMs, latestMs) { - return http$(`${basePath}/results/partition_fields_values`, { + return http$(`${basePath()}/results/partition_fields_values`, { method: 'POST', body: { jobId, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap index e8f7050f20875..2f5eb596a157b 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap @@ -14,7 +14,7 @@ exports[`NewCalendar Renders new calendar form 1`] = ` horizontalPosition="center" verticalPosition="center" > - - { + const msg = i18n.translate('xpack.ml.calendarsEdit.calendarForm.allowedCharactersDescription', { defaultMessage: 'Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores; ' + 'must start and end with an alphanumeric character', @@ -217,9 +216,9 @@ export const CalendarForm = injectI18n(function CalendarForm({ ); -}); +}; -CalendarForm.WrappedComponent.propTypes = { +CalendarForm.propTypes = { calendarId: PropTypes.string.isRequired, canCreateCalendar: PropTypes.bool.isRequired, canDeleteCalendar: PropTypes.bool.isRequired, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js index 6befb9987cba8..bc055bffe9973 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/chrome', () => ({ - getBasePath: jest.fn(), -})); - import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { CalendarForm } from './calendar_form'; @@ -39,7 +35,7 @@ const testProps = { describe('CalendarForm', () => { test('Renders calendar form', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); @@ -51,7 +47,7 @@ describe('CalendarForm', () => { calendarId: 'test-calendar', description: 'test description', }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const calendarId = wrapper.find('EuiTitle'); expect(calendarId).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js index 125c75d438af9..7a05a4ccb6aa7 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js @@ -10,7 +10,8 @@ import moment from 'moment'; import { EuiButton, EuiButtonEmpty, EuiInMemoryTable, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; @@ -32,7 +33,7 @@ function DeleteButton({ onClick, canDeleteCalendar }) { ); } -export const EventsTable = injectI18n(function EventsTable({ +export const EventsTable = ({ canCreateCalendar, canDeleteCalendar, eventsList, @@ -40,8 +41,7 @@ export const EventsTable = injectI18n(function EventsTable({ showSearchBar, showImportModal, showNewEventModal, - intl, -}) { +}) => { const sorting = { sort: { field: 'description', @@ -57,8 +57,7 @@ export const EventsTable = injectI18n(function EventsTable({ const columns = [ { field: 'description', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.eventsTable.descriptionColumnName', + name: i18n.translate('xpack.ml.calendarsEdit.eventsTable.descriptionColumnName', { defaultMessage: 'Description', }), sortable: true, @@ -67,8 +66,7 @@ export const EventsTable = injectI18n(function EventsTable({ }, { field: 'start_time', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.eventsTable.startColumnName', + name: i18n.translate('xpack.ml.calendarsEdit.eventsTable.startColumnName', { defaultMessage: 'Start', }), sortable: true, @@ -79,8 +77,7 @@ export const EventsTable = injectI18n(function EventsTable({ }, { field: 'end_time', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.eventsTable.endColumnName', + name: i18n.translate('xpack.ml.calendarsEdit.eventsTable.endColumnName', { defaultMessage: 'End', }), sortable: true, @@ -152,9 +149,9 @@ export const EventsTable = injectI18n(function EventsTable({ /> ); -}); +}; -EventsTable.WrappedComponent.propTypes = { +EventsTable.propTypes = { canCreateCalendar: PropTypes.bool, canDeleteCalendar: PropTypes.bool, eventsList: PropTypes.array.isRequired, @@ -164,7 +161,7 @@ EventsTable.WrappedComponent.propTypes = { showSearchBar: PropTypes.bool, }; -EventsTable.WrappedComponent.defaultProps = { +EventsTable.defaultProps = { showSearchBar: false, canCreateCalendar: true, canDeleteCalendar: true, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.test.js index 851ce52d68a36..8336a2d286639 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.test.js @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/chrome', () => ({ - getBasePath: jest.fn(), -})); - import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { EventsTable } from './events_table'; @@ -31,7 +27,7 @@ const testProps = { describe('EventsTable', () => { test('Renders events table with no search bar', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); @@ -42,7 +38,7 @@ describe('EventsTable', () => { showSearchBar: true, }; - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js index 5e2547ffa64e4..47644e329805c 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js @@ -23,191 +23,194 @@ import { import { ImportedEvents } from '../imported_events'; import { readFile, parseICSFile, filterEvents } from './utils'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; const MAX_FILE_SIZE_MB = 100; -export const ImportModal = injectI18n( - class ImportModal extends Component { - static propTypes = { - addImportedEvents: PropTypes.func.isRequired, - closeImportModal: PropTypes.func.isRequired, +export class ImportModal extends Component { + static propTypes = { + addImportedEvents: PropTypes.func.isRequired, + closeImportModal: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + includePastEvents: false, + allImportedEvents: [], + selectedEvents: [], + fileLoading: false, + fileLoaded: false, + errorMessage: null, }; + } - constructor(props) { - super(props); - - this.state = { - includePastEvents: false, - allImportedEvents: [], - selectedEvents: [], - fileLoading: false, - fileLoaded: false, - errorMessage: null, - }; - } - - handleImport = async loadedFile => { - const incomingFile = loadedFile[0]; - const errorMessage = this.props.intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.importModal.couldNotParseICSFileErrorMessage', + handleImport = async loadedFile => { + const incomingFile = loadedFile[0]; + const errorMessage = i18n.translate( + 'xpack.ml.calendarsEdit.importModal.couldNotParseICSFileErrorMessage', + { defaultMessage: 'Could not parse ICS file.', - }); - let events = []; - - if (incomingFile && incomingFile.size <= MAX_FILE_SIZE_MB * 1000000) { - this.setState({ fileLoading: true, fileLoaded: true }); - - try { - const parsedFile = await readFile(incomingFile); - events = parseICSFile(parsedFile.data); - - this.setState({ - allImportedEvents: events, - selectedEvents: filterEvents(events), - fileLoading: false, - errorMessage: null, - includePastEvents: false, - }); - } catch (error) { - console.log(errorMessage, error); - this.setState({ errorMessage, fileLoading: false }); - } - } else if (incomingFile && incomingFile.size > MAX_FILE_SIZE_MB * 1000000) { - this.setState({ fileLoading: false, errorMessage }); - } else { - this.setState({ fileLoading: false, errorMessage: null }); } - }; - - onEventDelete = eventId => { - this.setState(prevState => ({ - allImportedEvents: prevState.allImportedEvents.filter(event => event.event_id !== eventId), - selectedEvents: prevState.selectedEvents.filter(event => event.event_id !== eventId), - })); - }; - - onCheckboxToggle = e => { - this.setState({ - includePastEvents: e.target.checked, - }); - }; - - handleEventsAdd = () => { - const { allImportedEvents, selectedEvents, includePastEvents } = this.state; - const eventsToImport = includePastEvents ? allImportedEvents : selectedEvents; - - const events = eventsToImport.map(event => ({ - description: event.description, - start_time: event.start_time, - end_time: event.end_time, - event_id: event.event_id, - })); - - this.props.addImportedEvents(events); - }; - - renderCallout = () => ( - -

{this.state.errorMessage}

-
); - - render() { - const { closeImportModal, intl } = this.props; - const { - fileLoading, - fileLoaded, - allImportedEvents, - selectedEvents, - errorMessage, - includePastEvents, - } = this.state; - - let showRecurringWarning = false; - let importedEvents; - - if (includePastEvents) { - importedEvents = allImportedEvents; - } else { - importedEvents = selectedEvents; + let events = []; + + if (incomingFile && incomingFile.size <= MAX_FILE_SIZE_MB * 1000000) { + this.setState({ fileLoading: true, fileLoaded: true }); + + try { + const parsedFile = await readFile(incomingFile); + events = parseICSFile(parsedFile.data); + + this.setState({ + allImportedEvents: events, + selectedEvents: filterEvents(events), + fileLoading: false, + errorMessage: null, + includePastEvents: false, + }); + } catch (error) { + console.log(errorMessage, error); + this.setState({ errorMessage, fileLoading: false }); } + } else if (incomingFile && incomingFile.size > MAX_FILE_SIZE_MB * 1000000) { + this.setState({ fileLoading: false, errorMessage }); + } else { + this.setState({ fileLoading: false, errorMessage: null }); + } + }; + + onEventDelete = eventId => { + this.setState(prevState => ({ + allImportedEvents: prevState.allImportedEvents.filter(event => event.event_id !== eventId), + selectedEvents: prevState.selectedEvents.filter(event => event.event_id !== eventId), + })); + }; + + onCheckboxToggle = e => { + this.setState({ + includePastEvents: e.target.checked, + }); + }; + + handleEventsAdd = () => { + const { allImportedEvents, selectedEvents, includePastEvents } = this.state; + const eventsToImport = includePastEvents ? allImportedEvents : selectedEvents; + + const events = eventsToImport.map(event => ({ + description: event.description, + start_time: event.start_time, + end_time: event.end_time, + event_id: event.event_id, + })); + + this.props.addImportedEvents(events); + }; + + renderCallout = () => ( + +

{this.state.errorMessage}

+
+ ); + + render() { + const { closeImportModal } = this.props; + const { + fileLoading, + fileLoaded, + allImportedEvents, + selectedEvents, + errorMessage, + includePastEvents, + } = this.state; + + let showRecurringWarning = false; + let importedEvents; + + if (includePastEvents) { + importedEvents = allImportedEvents; + } else { + importedEvents = selectedEvents; + } - if (importedEvents.find(e => e.asterisk) !== undefined) { - showRecurringWarning = true; - } + if (importedEvents.find(e => e.asterisk) !== undefined) { + showRecurringWarning = true; + } - return ( - - - - - - - - - - -

- -

-
-
-
- - - - - + + + + + + - - {errorMessage !== null && this.renderCallout()} - {allImportedEvents.length > 0 && ( - + + +

+ - )} - - - - - - + + + + + + + + - - - + {errorMessage !== null && this.renderCallout()} + {allImportedEvents.length > 0 && ( + - - - - - ); - } + )} + + + + + + + + + + + + + + ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js index b689895b05671..d20dc9d297eb2 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js @@ -33,13 +33,13 @@ const events = [ describe('ImportModal', () => { test('Renders import modal', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('Deletes selected event from event table', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const testState = { allImportedEvents: events, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap index a4da960cbd627..a47405cd8de14 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap @@ -23,7 +23,9 @@ exports[`ImportedEvents Renders imported events 1`] = ` - ({ - getBasePath: jest.fn(), -})); - import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ImportedEvents } from './imported_events'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index bc60e9e5df24e..0489528fa0f63 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -6,14 +6,11 @@ import React, { Component, Fragment } from 'react'; import { PropTypes } from 'prop-types'; -import { timefilter } from 'ui/timefilter'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiPage, EuiPageBody, EuiPageContent, EuiOverlayMask } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; - import { NavigationMenu } from '../../../components/navigation_menu'; import { getCalendarSettingsData, validateCalendarId } from './utils'; @@ -21,357 +18,350 @@ import { CalendarForm } from './calendar_form'; import { NewEventModal } from './new_event_modal'; import { ImportModal } from './import_modal'; import { ml } from '../../../services/ml_api_service'; - -export const NewCalendar = injectI18n( - class NewCalendar extends Component { - static propTypes = { - calendarId: PropTypes.string, - canCreateCalendar: PropTypes.bool.isRequired, - canDeleteCalendar: PropTypes.bool.isRequired, +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; + +class NewCalendarUI extends Component { + static propTypes = { + calendarId: PropTypes.string, + canCreateCalendar: PropTypes.bool.isRequired, + canDeleteCalendar: PropTypes.bool.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + isNewEventModalVisible: false, + isImportModalVisible: false, + isNewCalendarIdValid: null, + loading: true, + jobIds: [], + jobIdOptions: [], + groupIds: [], + groupIdOptions: [], + calendars: [], + formCalendarId: '', + description: '', + selectedJobOptions: [], + selectedGroupOptions: [], + events: [], + saving: false, + selectedCalendar: undefined, }; + } - constructor(props) { - super(props); - this.state = { - isNewEventModalVisible: false, - isImportModalVisible: false, - isNewCalendarIdValid: null, - loading: true, - jobIds: [], - jobIdOptions: [], - groupIds: [], - groupIdOptions: [], - calendars: [], - formCalendarId: '', - description: '', - selectedJobOptions: [], - selectedGroupOptions: [], - events: [], - saving: false, - selectedCalendar: undefined, - }; - } - - componentDidMount() { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - this.formSetup(); - } + componentDidMount() { + const { timefilter } = this.props.kibana.services.data.query.timefilter; + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + this.formSetup(); + } - async formSetup() { - try { - const { jobIds, groupIds, calendars } = await getCalendarSettingsData(); - - const jobIdOptions = jobIds.map(jobId => ({ label: jobId })); - const groupIdOptions = groupIds.map(groupId => ({ label: groupId })); - - const selectedJobOptions = []; - const selectedGroupOptions = []; - let eventsList = []; - let selectedCalendar; - let formCalendarId = ''; - - // Editing existing calendar. - if (this.props.calendarId !== undefined) { - selectedCalendar = calendars.find(cal => cal.calendar_id === this.props.calendarId); - - if (selectedCalendar) { - formCalendarId = selectedCalendar.calendar_id; - eventsList = selectedCalendar.events; - - selectedCalendar.job_ids.forEach(id => { - if (jobIds.find(jobId => jobId === id)) { - selectedJobOptions.push({ label: id }); - } else if (groupIds.find(groupId => groupId === id)) { - selectedGroupOptions.push({ label: id }); - } - }); - } + async formSetup() { + try { + const { jobIds, groupIds, calendars } = await getCalendarSettingsData(); + + const jobIdOptions = jobIds.map(jobId => ({ label: jobId })); + const groupIdOptions = groupIds.map(groupId => ({ label: groupId })); + + const selectedJobOptions = []; + const selectedGroupOptions = []; + let eventsList = []; + let selectedCalendar; + let formCalendarId = ''; + + // Editing existing calendar. + if (this.props.calendarId !== undefined) { + selectedCalendar = calendars.find(cal => cal.calendar_id === this.props.calendarId); + + if (selectedCalendar) { + formCalendarId = selectedCalendar.calendar_id; + eventsList = selectedCalendar.events; + + selectedCalendar.job_ids.forEach(id => { + if (jobIds.find(jobId => jobId === id)) { + selectedJobOptions.push({ label: id }); + } else if (groupIds.find(groupId => groupId === id)) { + selectedGroupOptions.push({ label: id }); + } + }); } - - this.setState({ - events: eventsList, - formCalendarId, - jobIds, - jobIdOptions, - groupIds, - groupIdOptions, - calendars, - loading: false, - selectedJobOptions, - selectedGroupOptions, - selectedCalendar, - }); - } catch (error) { - console.log(error); - this.setState({ loading: false }); - toastNotifications.addDanger( - this.props.intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.errorWithLoadingCalendarFromDataErrorMessage', - defaultMessage: - 'An error occurred loading calendar form data. Try refreshing the page.', - }) - ); } + + this.setState({ + events: eventsList, + formCalendarId, + jobIds, + jobIdOptions, + groupIds, + groupIdOptions, + calendars, + loading: false, + selectedJobOptions, + selectedGroupOptions, + selectedCalendar, + }); + } catch (error) { + console.log(error); + this.setState({ loading: false }); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsEdit.errorWithLoadingCalendarFromDataErrorMessage', { + defaultMessage: 'An error occurred loading calendar form data. Try refreshing the page.', + }) + ); } + } - isDuplicateId = () => { - const { calendars, formCalendarId } = this.state; + isDuplicateId = () => { + const { calendars, formCalendarId } = this.state; - for (let i = 0; i < calendars.length; i++) { - if (calendars[i].calendar_id === formCalendarId) { - return true; - } + for (let i = 0; i < calendars.length; i++) { + if (calendars[i].calendar_id === formCalendarId) { + return true; } + } - return false; - }; + return false; + }; - onCreate = async () => { - const { formCalendarId } = this.state; - const { intl } = this.props; - - if (this.isDuplicateId()) { - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.calendarsEdit.canNotCreateCalendarWithExistingIdErrorMessag', - defaultMessage: - 'Cannot create calendar with id [{formCalendarId}] as it already exists.', - }, - { formCalendarId } - ) - ); - } else { - const calendar = this.setUpCalendarForApi(); - this.setState({ saving: true }); - - try { - await ml.addCalendar(calendar); - window.location = '#/settings/calendars_list'; - } catch (error) { - console.log('Error saving calendar', error); - this.setState({ saving: false }); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.calendarsEdit.errorWithCreatingCalendarErrorMessage', - defaultMessage: 'An error occurred creating calendar {calendarId}', - }, - { calendarId: calendar.calendarId } - ) - ); - } - } - }; + onCreate = async () => { + const { formCalendarId } = this.state; - onEdit = async () => { + if (this.isDuplicateId()) { + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsEdit.canNotCreateCalendarWithExistingIdErrorMessag', { + defaultMessage: 'Cannot create calendar with id [{formCalendarId}] as it already exists.', + values: { formCalendarId }, + }) + ); + } else { const calendar = this.setUpCalendarForApi(); this.setState({ saving: true }); try { - await ml.updateCalendar(calendar); + await ml.addCalendar(calendar); window.location = '#/settings/calendars_list'; } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.ml.calendarsEdit.errorWithUpdatingCalendarErrorMessage', - defaultMessage: - 'An error occurred saving calendar {calendarId}. Try refreshing the page.', - }, - { calendarId: calendar.calendarId } - ) + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsEdit.errorWithCreatingCalendarErrorMessage', { + defaultMessage: 'An error occurred creating calendar {calendarId}', + values: { calendarId: calendar.calendarId }, + }) ); } + } + }; + + onEdit = async () => { + const calendar = this.setUpCalendarForApi(); + this.setState({ saving: true }); + + try { + await ml.updateCalendar(calendar); + window.location = '#/settings/calendars_list'; + } catch (error) { + console.log('Error saving calendar', error); + this.setState({ saving: false }); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsEdit.errorWithUpdatingCalendarErrorMessage', { + defaultMessage: + 'An error occurred saving calendar {calendarId}. Try refreshing the page.', + values: { calendarId: calendar.calendarId }, + }) + ); + } + }; + + setUpCalendarForApi = () => { + const { + formCalendarId, + description, + events, + selectedGroupOptions, + selectedJobOptions, + } = this.state; + + const jobIds = selectedJobOptions.map(option => option.label); + const groupIds = selectedGroupOptions.map(option => option.label); + + // Reduce events to fields expected by api + const eventsToSave = events.map(event => ({ + description: event.description, + start_time: event.start_time, + end_time: event.end_time, + })); + + // set up calendar + const calendar = { + calendarId: formCalendarId, + description, + events: eventsToSave, + job_ids: [...jobIds, ...groupIds], }; - setUpCalendarForApi = () => { - const { - formCalendarId, - description, - events, - selectedGroupOptions, - selectedJobOptions, - } = this.state; - - const jobIds = selectedJobOptions.map(option => option.label); - const groupIds = selectedGroupOptions.map(option => option.label); - - // Reduce events to fields expected by api - const eventsToSave = events.map(event => ({ - description: event.description, - start_time: event.start_time, - end_time: event.end_time, - })); - - // set up calendar - const calendar = { - calendarId: formCalendarId, - description, - events: eventsToSave, - job_ids: [...jobIds, ...groupIds], - }; - - return calendar; - }; - - onCreateGroupOption = newGroup => { - const newOption = { - label: newGroup, - }; - // Select the option. - this.setState(prevState => ({ - selectedGroupOptions: prevState.selectedGroupOptions.concat(newOption), - })); - }; - - onJobSelection = selectedJobOptions => { - this.setState({ - selectedJobOptions, - }); - }; - - onGroupSelection = selectedGroupOptions => { - this.setState({ - selectedGroupOptions, - }); - }; - - onCalendarIdChange = e => { - const isValid = validateCalendarId(e.target.value); - - this.setState({ - formCalendarId: e.target.value, - isNewCalendarIdValid: isValid, - }); - }; - - onDescriptionChange = e => { - this.setState({ - description: e.target.value, - }); - }; - - showImportModal = () => { - this.setState(prevState => ({ - isImportModalVisible: !prevState.isImportModalVisible, - })); - }; - - closeImportModal = () => { - this.setState({ - isImportModalVisible: false, - }); - }; - - onEventDelete = eventId => { - this.setState(prevState => ({ - events: prevState.events.filter(event => event.event_id !== eventId), - })); - }; - - closeNewEventModal = () => { - this.setState({ isNewEventModalVisible: false }); - }; - - showNewEventModal = () => { - this.setState({ isNewEventModalVisible: true }); - }; - - addEvent = event => { - this.setState(prevState => ({ - events: [...prevState.events, event], - isNewEventModalVisible: false, - })); - }; + return calendar; + }; - addImportedEvents = events => { - this.setState(prevState => ({ - events: [...prevState.events, ...events], - isImportModalVisible: false, - })); + onCreateGroupOption = newGroup => { + const newOption = { + label: newGroup, }; - - render() { - const { - events, - isNewEventModalVisible, - isImportModalVisible, - isNewCalendarIdValid, - formCalendarId, - description, - groupIdOptions, - jobIdOptions, - saving, - selectedCalendar, - selectedJobOptions, - selectedGroupOptions, - } = this.state; - - let modal = ''; - - if (isNewEventModalVisible) { - modal = ( - - - - ); - } else if (isImportModalVisible) { - modal = ( - - - - ); - } - - return ( - - - - - - - - {modal} - - - + // Select the option. + this.setState(prevState => ({ + selectedGroupOptions: prevState.selectedGroupOptions.concat(newOption), + })); + }; + + onJobSelection = selectedJobOptions => { + this.setState({ + selectedJobOptions, + }); + }; + + onGroupSelection = selectedGroupOptions => { + this.setState({ + selectedGroupOptions, + }); + }; + + onCalendarIdChange = e => { + const isValid = validateCalendarId(e.target.value); + + this.setState({ + formCalendarId: e.target.value, + isNewCalendarIdValid: isValid, + }); + }; + + onDescriptionChange = e => { + this.setState({ + description: e.target.value, + }); + }; + + showImportModal = () => { + this.setState(prevState => ({ + isImportModalVisible: !prevState.isImportModalVisible, + })); + }; + + closeImportModal = () => { + this.setState({ + isImportModalVisible: false, + }); + }; + + onEventDelete = eventId => { + this.setState(prevState => ({ + events: prevState.events.filter(event => event.event_id !== eventId), + })); + }; + + closeNewEventModal = () => { + this.setState({ isNewEventModalVisible: false }); + }; + + showNewEventModal = () => { + this.setState({ isNewEventModalVisible: true }); + }; + + addEvent = event => { + this.setState(prevState => ({ + events: [...prevState.events, event], + isNewEventModalVisible: false, + })); + }; + + addImportedEvents = events => { + this.setState(prevState => ({ + events: [...prevState.events, ...events], + isImportModalVisible: false, + })); + }; + + render() { + const { + events, + isNewEventModalVisible, + isImportModalVisible, + isNewCalendarIdValid, + formCalendarId, + description, + groupIdOptions, + jobIdOptions, + saving, + selectedCalendar, + selectedJobOptions, + selectedGroupOptions, + } = this.state; + + let modal = ''; + + if (isNewEventModalVisible) { + modal = ( + + + + ); + } else if (isImportModalVisible) { + modal = ( + + + ); } + + return ( + + + + + + + + {modal} + + + + ); } -); +} + +export const NewCalendar = withKibana(NewCalendarUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index e8999053a93bb..8dc174040f9c8 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -47,10 +47,9 @@ jest.mock('./utils', () => ({ }) ), })); -jest.mock('ui/timefilter', () => ({ - timefilter: { - disableTimeRangeSelector: jest.fn(), - disableAutoRefreshSelector: jest.fn(), +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: comp => { + return comp; }, })); @@ -92,17 +91,31 @@ const calendars = [ const props = { canCreateCalendar: true, canDeleteCalendar: true, + kibana: { + services: { + data: { + query: { + timefilter: { + timefilter: { + disableTimeRangeSelector: jest.fn(), + disableAutoRefreshSelector: jest.fn(), + }, + }, + }, + }, + }, + }, }; describe('NewCalendar', () => { test('Renders new calendar form', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('Import modal shown on Import Events button click', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const importButton = wrapper.find('[data-testid="ml_import_events"]'); const button = importButton.find('EuiButton'); @@ -112,7 +125,7 @@ describe('NewCalendar', () => { }); test('New event modal shown on New event button click', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const importButton = wrapper.find('[data-testid="ml_new_event"]'); const button = importButton.find('EuiButton'); @@ -122,7 +135,7 @@ describe('NewCalendar', () => { }); test('isDuplicateId returns true if form calendar id already exists in calendars', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const instance = wrapper.instance(); instance.setState({ @@ -139,7 +152,7 @@ describe('NewCalendar', () => { canCreateCalendar: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const buttons = wrapper.find('[data-testid="ml_save_calendar_button"]'); const saveButton = buttons.find('EuiButton'); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js index 4efcf8e441c1e..814f30a70db54 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js @@ -27,290 +27,289 @@ import moment from 'moment'; import { TIME_FORMAT } from '../events_table'; import { generateTempId } from '../utils'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; const VALID_DATE_STRING_LENGTH = 19; -export const NewEventModal = injectI18n( - class NewEventModal extends Component { - static propTypes = { - closeModal: PropTypes.func.isRequired, - addEvent: PropTypes.func.isRequired, +export class NewEventModal extends Component { + static propTypes = { + closeModal: PropTypes.func.isRequired, + addEvent: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + const startDate = moment().startOf('day'); + const endDate = moment() + .startOf('day') + .add(1, 'days'); + + this.state = { + startDate, + endDate, + description: '', + startDateString: startDate.format(TIME_FORMAT), + endDateString: endDate.format(TIME_FORMAT), }; + } - constructor(props) { - super(props); - - const startDate = moment().startOf('day'); - const endDate = moment() - .startOf('day') - .add(1, 'days'); - - this.state = { - startDate, - endDate, - description: '', - startDateString: startDate.format(TIME_FORMAT), - endDateString: endDate.format(TIME_FORMAT), - }; - } - - onDescriptionChange = e => { - this.setState({ - description: e.target.value, - }); + onDescriptionChange = e => { + this.setState({ + description: e.target.value, + }); + }; + + handleAddEvent = () => { + const { description, startDate, endDate } = this.state; + // Temp reference to unsaved events to allow removal from table + const tempId = generateTempId(); + + const event = { + description, + start_time: startDate.valueOf(), + end_time: endDate.valueOf(), + event_id: tempId, }; - handleAddEvent = () => { - const { description, startDate, endDate } = this.state; - // Temp reference to unsaved events to allow removal from table - const tempId = generateTempId(); - - const event = { - description, - start_time: startDate.valueOf(), - end_time: endDate.valueOf(), - event_id: tempId, - }; + this.props.addEvent(event); + }; - this.props.addEvent(event); - }; + handleChangeStart = date => { + let start = null; + let end = this.state.endDate; - handleChangeStart = date => { - let start = null; - let end = this.state.endDate; + const startMoment = moment(date); + const endMoment = moment(date); - const startMoment = moment(date); - const endMoment = moment(date); + start = startMoment.startOf('day'); - start = startMoment.startOf('day'); + if (start > end) { + end = endMoment.startOf('day').add(1, 'days'); + } + this.setState({ + startDate: start, + endDate: end, + startDateString: start.format(TIME_FORMAT), + endDateString: end.format(TIME_FORMAT), + }); + }; - if (start > end) { - end = endMoment.startOf('day').add(1, 'days'); - } - this.setState({ - startDate: start, - endDate: end, - startDateString: start.format(TIME_FORMAT), - endDateString: end.format(TIME_FORMAT), - }); - }; + handleChangeEnd = date => { + let start = this.state.startDate; + let end = null; - handleChangeEnd = date => { - let start = this.state.startDate; - let end = null; + const startMoment = moment(date); + const endMoment = moment(date); - const startMoment = moment(date); - const endMoment = moment(date); + end = endMoment.startOf('day'); - end = endMoment.startOf('day'); + if (start > end) { + start = startMoment.startOf('day').subtract(1, 'days'); + } + this.setState({ + startDate: start, + endDate: end, + startDateString: start.format(TIME_FORMAT), + endDateString: end.format(TIME_FORMAT), + }); + }; + + handleTimeStartChange = event => { + const dateString = event.target.value; + let isValidDate = false; + + if (dateString.length === VALID_DATE_STRING_LENGTH) { + isValidDate = moment(dateString).isValid(TIME_FORMAT, true); + } else { + this.setState({ + startDateString: dateString, + }); + } - if (start > end) { - start = startMoment.startOf('day').subtract(1, 'days'); - } + if (isValidDate) { this.setState({ - startDate: start, - endDate: end, - startDateString: start.format(TIME_FORMAT), - endDateString: end.format(TIME_FORMAT), + startDateString: dateString, + startDate: moment(dateString), }); - }; + } + }; - handleTimeStartChange = event => { - const dateString = event.target.value; - let isValidDate = false; - - if (dateString.length === VALID_DATE_STRING_LENGTH) { - isValidDate = moment(dateString).isValid(TIME_FORMAT, true); - } else { - this.setState({ - startDateString: dateString, - }); - } - - if (isValidDate) { - this.setState({ - startDateString: dateString, - startDate: moment(dateString), - }); - } - }; + handleTimeEndChange = event => { + const dateString = event.target.value; + let isValidDate = false; - handleTimeEndChange = event => { - const dateString = event.target.value; - let isValidDate = false; - - if (dateString.length === VALID_DATE_STRING_LENGTH) { - isValidDate = moment(dateString).isValid(TIME_FORMAT, true); - } else { - this.setState({ - endDateString: dateString, - }); - } - - if (isValidDate) { - this.setState({ - endDateString: dateString, - endDate: moment(dateString), - }); - } - }; + if (dateString.length === VALID_DATE_STRING_LENGTH) { + isValidDate = moment(dateString).isValid(TIME_FORMAT, true); + } else { + this.setState({ + endDateString: dateString, + }); + } - renderRangedDatePicker = () => { - const { startDate, endDate, startDateString, endDateString } = this.state; + if (isValidDate) { + this.setState({ + endDateString: dateString, + endDate: moment(dateString), + }); + } + }; - const { intl } = this.props; + renderRangedDatePicker = () => { + const { startDate, endDate, startDateString, endDateString } = this.state; - const timeInputs = ( - - - - - } - helpText={TIME_FORMAT} - > - + + + - - - + } + helpText={TIME_FORMAT} + > + + + + + + } + helpText={TIME_FORMAT} + > + + + + + + ); + + return ( + + + {timeInputs} + + + endDate} + aria-label={i18n.translate( + 'xpack.ml.calendarsEdit.newEventModal.startDateAriaLabel', + { + defaultMessage: 'Start date', + } + )} + timeFormat={TIME_FORMAT} + dateFormat={TIME_FORMAT} + /> + } + endDateControl={ + endDate} + aria-label={i18n.translate( + 'xpack.ml.calendarsEdit.newEventModal.endDateAriaLabel', + { defaultMessage: 'End date' } + )} + timeFormat={TIME_FORMAT} + dateFormat={TIME_FORMAT} + /> + } + /> + + + ); + }; + + render() { + const { closeModal } = this.props; + const { description } = this.state; + + return ( + + + + + + + + + + } - helpText={TIME_FORMAT} + fullWidth > - - - - - ); - - return ( - - - {timeInputs} - - - endDate} - aria-label={intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.newEventModal.startDateAriaLabel', - defaultMessage: 'Start date', - })} - timeFormat={TIME_FORMAT} - dateFormat={TIME_FORMAT} - /> - } - endDateControl={ - endDate} - aria-label={intl.formatMessage({ - id: 'xpack.ml.calendarsEdit.newEventModal.endDateAriaLabel', - defaultMessage: 'End date', - })} - timeFormat={TIME_FORMAT} - dateFormat={TIME_FORMAT} /> - } - /> - - - ); - }; - - render() { - const { closeModal } = this.props; - const { description } = this.state; - - return ( - - - - - - - - - - - - } - fullWidth - > - - - - - - {this.renderRangedDatePicker()} - - + - - - - - - - - - - - ); - } + + + {this.renderRangedDatePicker()} + + + + + + + + + + + + + + ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js index bbb64584d8e1e..e91dce6124cef 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js @@ -21,14 +21,14 @@ const stateTimestamps = { describe('NewEventModal', () => { it('Add button disabled if description empty', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); const addButton = wrapper.find('EuiButton').first(); expect(addButton.prop('disabled')).toBe(true); }); it('if endDate is less than startDate should set startDate one day before endDate', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); const instance = wrapper.instance(); instance.setState({ startDate: moment(stateTimestamps.startDate), @@ -51,7 +51,7 @@ describe('NewEventModal', () => { }); it('if startDate is greater than endDate should set endDate one day after startDate', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); const instance = wrapper.instance(); instance.setState({ startDate: moment(stateTimestamps.startDate), diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap index 867fd16932627..aeeeeef63a71e 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap @@ -14,11 +14,11 @@ exports[`CalendarsList Renders calendar list with calendars 1`] = ` horizontalPosition="center" verticalPosition="center" > - - { + this.setState({ loading: true }); - constructor(props) { - super(props); - this.state = { - loading: true, - calendars: [], + try { + const calendars = await ml.calendars(); + + this.setState({ + calendars, + loading: false, isDestroyModalVisible: false, - calendarId: null, - selectedForDeletion: [], - nodesAvailable: mlNodesAvailable(), - }; + }); + } catch (error) { + console.log(error); + this.setState({ loading: false }); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage', { + defaultMessage: 'An error occurred loading the list of calendars.', + }) + ); } + }; - loadCalendars = async () => { - this.setState({ loading: true }); - - try { - const calendars = await ml.calendars(); - - this.setState({ - calendars, - loading: false, - isDestroyModalVisible: false, - }); - } catch (error) { - console.log(error); - this.setState({ loading: false }); - toastNotifications.addDanger( - this.props.intl.formatMessage({ - id: 'xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage', - defaultMessage: 'An error occurred loading the list of calendars.', - }) - ); - } - }; + closeDestroyModal = () => { + this.setState({ isDestroyModalVisible: false, calendarId: null }); + }; - closeDestroyModal = () => { - this.setState({ isDestroyModalVisible: false, calendarId: null }); - }; + showDestroyModal = () => { + this.setState({ isDestroyModalVisible: true }); + }; - showDestroyModal = () => { - this.setState({ isDestroyModalVisible: true }); - }; + setSelectedCalendarList = selectedCalendars => { + this.setState({ selectedForDeletion: selectedCalendars }); + }; - setSelectedCalendarList = selectedCalendars => { - this.setState({ selectedForDeletion: selectedCalendars }); - }; + deleteCalendars = () => { + const { selectedForDeletion } = this.state; - deleteCalendars = () => { - const { selectedForDeletion } = this.state; + this.closeDestroyModal(); + deleteCalendars(selectedForDeletion, this.loadCalendars); + }; - this.closeDestroyModal(); - deleteCalendars(selectedForDeletion, this.loadCalendars); - }; + addRequiredFieldsToList = (calendarsList = []) => { + for (let i = 0; i < calendarsList.length; i++) { + calendarsList[i].job_ids_string = calendarsList[i].job_ids.join(', '); + calendarsList[i].events_length = calendarsList[i].events.length; + } - addRequiredFieldsToList = (calendarsList = []) => { - for (let i = 0; i < calendarsList.length; i++) { - calendarsList[i].job_ids_string = calendarsList[i].job_ids.join(', '); - calendarsList[i].events_length = calendarsList[i].events.length; - } + return calendarsList; + }; - return calendarsList; - }; + componentDidMount() { + this.loadCalendars(); + } - componentDidMount() { - this.loadCalendars(); + render() { + const { calendars, selectedForDeletion, loading, nodesAvailable } = this.state; + const { canCreateCalendar, canDeleteCalendar } = this.props; + let destroyModal = ''; + + if (this.state.isDestroyModalVisible) { + destroyModal = ( + + + } + onCancel={this.closeDestroyModal} + onConfirm={this.deleteCalendars} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + > +

+ c.calendar_id).join(', '), + }} + /> +

+ + + ); } - render() { - const { calendars, selectedForDeletion, loading, nodesAvailable } = this.state; - const { canCreateCalendar, canDeleteCalendar } = this.props; - let destroyModal = ''; - - if (this.state.isDestroyModalVisible) { - destroyModal = ( - - - } - onCancel={this.closeDestroyModal} - onConfirm={this.deleteCalendars} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + return ( + + + + + -

- c.calendar_id).join(', '), - }} - /> -

-
-
- ); - } - - return ( - - - - - - - 0} - /> - - {destroyModal} - - - - ); - } + + 0} + /> + + {destroyModal} + + +
+ ); } -); +} + +export const CalendarsList = withKibana(CalendarsListUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js index 5e4e2c1e0d31e..677703bceeca7 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ml } from '../../../services/ml_api_service'; import { CalendarsList } from './calendars_list'; @@ -35,6 +35,17 @@ jest.mock('../../../services/ml_api_service', () => ({ }, })); +jest.mock('react', () => { + const r = jest.requireActual('react'); + return { ...r, memo: x => x }; +}); + +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: node => { + return node; + }, +})); + const testingState = { loading: false, calendars: [ @@ -76,34 +87,43 @@ const testingState = { const props = { canCreateCalendar: true, canDeleteCalendar: true, + kibana: { + services: { + data: { + query: { + timefilter: { + timefilter: { + disableTimeRangeSelector: jest.fn(), + disableAutoRefreshSelector: jest.fn(), + }, + }, + }, + }, + notifications: { + toasts: { + addDanger: () => {}, + }, + }, + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }, + }, + }, }; describe('CalendarsList', () => { test('loads calendars on mount', () => { ml.calendars = jest.fn(() => []); - shallowWithIntl(); + shallowWithIntl(); expect(ml.calendars).toHaveBeenCalled(); }); test('Renders calendar list with calendars', () => { - const wrapper = shallowWithIntl(); - + const wrapper = shallowWithIntl(); wrapper.instance().setState(testingState); wrapper.update(); expect(wrapper).toMatchSnapshot(); }); - - test('Sets selected calendars list on checkbox change', () => { - const wrapper = mountWithIntl(); - - const instance = wrapper.instance(); - const spy = jest.spyOn(instance, 'setSelectedCalendarList'); - instance.setState(testingState); - wrapper.update(); - - const checkbox = wrapper.find('input[type="checkbox"]').first(); - checkbox.simulate('change'); - expect(spy).toHaveBeenCalled(); - }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js index d1dbad0a85c06..f06812b2a9128 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../util/dependency_cache'; import { ml } from '../../../services/ml_api_service'; import { i18n } from '@kbn/i18n'; @@ -12,6 +12,7 @@ export async function deleteCalendars(calendarsToDelete, callback) { if (calendarsToDelete === undefined || calendarsToDelete.length === 0) { return; } + const toastNotifications = getToastNotifications(); // Delete each of the specified calendars in turn, waiting for each response // before deleting the next to minimize load on the cluster. diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js index 58f0ac268fdb2..b97b918f03f74 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js @@ -23,12 +23,12 @@ import { EuiButtonEmpty, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -// metadata.branch corresponds to the version used in documentation links. -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-calendars.html`; +function CalendarsListHeaderUI({ totalCount, refreshCalendars, kibana }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = kibana.services.docLinks; -export function CalendarsListHeader({ totalCount, refreshCalendars }) { + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-calendars.html`; return ( @@ -99,7 +99,9 @@ export function CalendarsListHeader({ totalCount, refreshCalendars }) { ); } -CalendarsListHeader.propTypes = { +CalendarsListHeaderUI.propTypes = { totalCount: PropTypes.number.isRequired, refreshCalendars: PropTypes.func.isRequired, }; + +export const CalendarsListHeader = withKibana(CalendarsListHeaderUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js index 583c9fe7276ae..d0c3619f55919 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js @@ -9,12 +9,26 @@ import React from 'react'; import { CalendarsListHeader } from './header'; +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: comp => { + return comp; + }, +})); + describe('CalendarListsHeader', () => { const refreshCalendars = jest.fn(() => {}); const requiredProps = { totalCount: 3, refreshCalendars, + kibana: { + services: { + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'jest-metadata-mock-branch', + }, + }, + }, }; test('renders header', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js index 774cc96517cc6..bd1dafcd6c0aa 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -9,9 +9,10 @@ import React from 'react'; import { EuiButton, EuiLink, EuiInMemoryTable } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -export const CalendarsListTable = injectI18n(function CalendarsListTable({ +export const CalendarsListTable = ({ calendarsList, onDeleteClick, setSelectedCalendarList, @@ -20,8 +21,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ canDeleteCalendar, mlNodesAvailable, itemsSelected, - intl, -}) { +}) => { const sorting = { sort: { field: 'calendar_id', @@ -37,8 +37,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ const columns = [ { field: 'calendar_id', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsList.table.idColumnName', + name: i18n.translate('xpack.ml.calendarsList.table.idColumnName', { defaultMessage: 'ID', }), sortable: true, @@ -48,8 +47,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ }, { field: 'job_ids_string', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsList.table.jobsColumnName', + name: i18n.translate('xpack.ml.calendarsList.table.jobsColumnName', { defaultMessage: 'Jobs', }), sortable: true, @@ -57,19 +55,15 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ }, { field: 'events_length', - name: intl.formatMessage({ - id: 'xpack.ml.calendarsList.table.eventsColumnName', + name: i18n.translate('xpack.ml.calendarsList.table.eventsColumnName', { defaultMessage: 'Events', }), sortable: true, render: eventsLength => - intl.formatMessage( - { - id: 'xpack.ml.calendarsList.table.eventsCountLabel', - defaultMessage: '{eventsLength, plural, one {# event} other {# events}}', - }, - { eventsLength } - ), + i18n.translate('xpack.ml.calendarsList.table.eventsCountLabel', { + defaultMessage: '{eventsLength, plural, one {# event} other {# events}}', + values: { eventsLength }, + }), }, ]; @@ -125,9 +119,9 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ /> ); -}); +}; -CalendarsListTable.WrappedComponent.propTypes = { +CalendarsListTable.propTypes = { calendarsList: PropTypes.array.isRequired, onDeleteClick: PropTypes.func.isRequired, loading: PropTypes.bool.isRequired, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js index 4d452309993a8..a4c5539d51d1b 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js @@ -9,10 +9,6 @@ import React from 'react'; import { CalendarsListTable } from './table'; -jest.mock('ui/chrome', () => ({ - getBasePath: jest.fn(), -})); - const calendars = [ { calendar_id: 'farequote-calendar', @@ -41,12 +37,12 @@ const props = { describe('CalendarsListTable', () => { test('renders the table with all calendars', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); test('New button enabled if permission available', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); @@ -60,7 +56,7 @@ describe('CalendarsListTable', () => { canCreateCalendar: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); @@ -74,7 +70,7 @@ describe('CalendarsListTable', () => { mlNodesAvailable: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js index 68911d503966b..c6d1c239d3406 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../../util/dependency_cache'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../services/ml_api_service'; @@ -13,6 +13,8 @@ export async function deleteFilterLists(filterListsToDelete) { return; } + const toastNotifications = getToastNotifications(); + // Delete each of the specified filter lists in turn, waiting for each response // before deleting the next to minimize load on the cluster. toastNotifications.add( diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js index f91eec2ec996e..e1e32afe08dbe 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js @@ -13,100 +13,100 @@ import React, { Component } from 'react'; import { EuiButtonIcon, EuiPopover, EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -export const EditDescriptionPopover = injectI18n( - class extends Component { - static displayName = 'EditDescriptionPopover'; - static propTypes = { - description: PropTypes.string, - updateDescription: PropTypes.func.isRequired, - canCreateFilter: PropTypes.bool.isRequired, +export class EditDescriptionPopover extends Component { + static displayName = 'EditDescriptionPopover'; + static propTypes = { + description: PropTypes.string, + updateDescription: PropTypes.func.isRequired, + canCreateFilter: PropTypes.bool.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + isPopoverOpen: false, + value: props.description, }; + } - constructor(props) { - super(props); + onChange = e => { + this.setState({ + value: e.target.value, + }); + }; - this.state = { - isPopoverOpen: false, - value: props.description, - }; + onButtonClick = () => { + if (this.state.isPopoverOpen === false) { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + value: this.props.description, + }); + } else { + this.closePopover(); } + }; - onChange = e => { + closePopover = () => { + if (this.state.isPopoverOpen === true) { this.setState({ - value: e.target.value, + isPopoverOpen: false, }); - }; - - onButtonClick = () => { - if (this.state.isPopoverOpen === false) { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - value: this.props.description, - }); - } else { - this.closePopover(); - } - }; - - closePopover = () => { - if (this.state.isPopoverOpen === true) { - this.setState({ - isPopoverOpen: false, - }); - this.props.updateDescription(this.state.value); - } - }; + this.props.updateDescription(this.state.value); + } + }; - render() { - const { isPopoverOpen, value } = this.state; - const { intl } = this.props; + render() { + const { isPopoverOpen, value } = this.state; - const button = ( - - ); + } + )} + isDisabled={this.props.canCreateFilter === false} + /> + ); - return ( -
- -
- - - } - > - + +
+ + - - -
-
-
- ); - } + } + > + + + +
+ +
+ ); } -); +} diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js index 43234dbc7bdc7..f97bfe6682f5e 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js @@ -16,7 +16,7 @@ function prepareTest(updateDescriptionFn) { canCreateFilter: true, }; - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); return wrapper; } @@ -30,7 +30,7 @@ describe('FilterListUsagePopover', () => { canCreateFilter: true, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap index 85a31fbcd9185..074654dc754fc 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap @@ -93,7 +93,7 @@ exports[`EditFilterListHeader renders the header when creating a new filter list - - @@ -300,7 +300,7 @@ exports[`EditFilterListHeader renders the header when editing an existing unused - @@ -397,7 +397,7 @@ exports[`EditFilterListHeader renders the header when editing an existing used f - { - const { intl } = this.props; - - ml.filters - .filters({ filterId }) - .then(filter => { - this.setLoadedFilterState(filter); - }) - .catch(resp => { - console.log(`Error loading filter ${filterId}:`, resp); - toastNotifications.addDanger( - intl.formatMessage( - { - id: - 'xpack.ml.settings.filterLists.editFilterList.loadingDetailsOfFilterErrorMessage', - defaultMessage: 'An error occurred loading details of filter {filterId}', - }, - { + loadFilterList = filterId => { + ml.filters + .filters({ filterId }) + .then(filter => { + this.setLoadedFilterState(filter); + }) + .catch(resp => { + console.log(`Error loading filter ${filterId}:`, resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate( + 'xpack.ml.settings.filterLists.editFilterList.loadingDetailsOfFilterErrorMessage', + { + defaultMessage: 'An error occurred loading details of filter {filterId}', + values: { filterId, - } - ) - ); - }); - }; - - setLoadedFilterState = loadedFilter => { - // Store the loaded filter so we can diff changes to the items when saving updates. - this.setState(prevState => { - const { itemsPerPage, searchQuery } = prevState; - - const matchingItems = getMatchingFilterItems(searchQuery, loadedFilter.items); - const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - - return { - description: loadedFilter.description, - items: loadedFilter.items !== undefined ? [...loadedFilter.items] : [], - matchingItems, - selectedItems: [], - loadedFilter, - isNewFilterIdInvalid: false, - activePage, - searchQuery, - saveInProgress: false, - }; + }, + } + ) + ); }); - }; + }; - updateNewFilterId = newFilterId => { - this.setState({ - newFilterId, - isNewFilterIdInvalid: !isValidFilterListId(newFilterId), - }); - }; + setLoadedFilterState = loadedFilter => { + // Store the loaded filter so we can diff changes to the items when saving updates. + this.setState(prevState => { + const { itemsPerPage, searchQuery } = prevState; - updateDescription = description => { - this.setState({ description }); - }; + const matchingItems = getMatchingFilterItems(searchQuery, loadedFilter.items); + const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - addItems = itemsToAdd => { - const { intl } = this.props; - - this.setState(prevState => { - const { itemsPerPage, searchQuery } = prevState; - const items = [...prevState.items]; - const alreadyInFilter = []; - itemsToAdd.forEach(item => { - if (items.indexOf(item) === -1) { - items.push(item); - } else { - alreadyInFilter.push(item); - } - }); - items.sort((str1, str2) => { - return str1.localeCompare(str2); - }); - - if (alreadyInFilter.length > 0) { - toastNotifications.addWarning( - intl.formatMessage( - { - id: - 'xpack.ml.settings.filterLists.editFilterList.duplicatedItemsInFilterListWarningMessage', - defaultMessage: - 'The following items were already in the filter list: {alreadyInFilter}', - }, - { - alreadyInFilter, - } - ) - ); + return { + description: loadedFilter.description, + items: loadedFilter.items !== undefined ? [...loadedFilter.items] : [], + matchingItems, + selectedItems: [], + loadedFilter, + isNewFilterIdInvalid: false, + activePage, + searchQuery, + saveInProgress: false, + }; + }); + }; + + updateNewFilterId = newFilterId => { + this.setState({ + newFilterId, + isNewFilterIdInvalid: !isValidFilterListId(newFilterId), + }); + }; + + updateDescription = description => { + this.setState({ description }); + }; + + addItems = itemsToAdd => { + this.setState(prevState => { + const { itemsPerPage, searchQuery } = prevState; + const items = [...prevState.items]; + const alreadyInFilter = []; + itemsToAdd.forEach(item => { + if (items.indexOf(item) === -1) { + items.push(item); + } else { + alreadyInFilter.push(item); } - - const matchingItems = getMatchingFilterItems(searchQuery, items); - const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - - return { - items, - matchingItems, - activePage, - searchQuery, - }; }); - }; - - deleteSelectedItems = () => { - this.setState(prevState => { - const { selectedItems, itemsPerPage, searchQuery } = prevState; - const items = [...prevState.items]; - selectedItems.forEach(item => { - const index = items.indexOf(item); - if (index !== -1) { - items.splice(index, 1); - } - }); - - const matchingItems = getMatchingFilterItems(searchQuery, items); - const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - - return { - items, - matchingItems, - selectedItems: [], - activePage, - searchQuery, - }; + items.sort((str1, str2) => { + return str1.localeCompare(str2); }); - }; - onSearchChange = ({ query }) => { - this.setState(prevState => { - const { items, itemsPerPage } = prevState; - - const matchingItems = getMatchingFilterItems(query, items); - const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); + if (alreadyInFilter.length > 0) { + const { toasts } = this.props.kibana.services.notifications; + toasts.addWarning( + i18n.translate( + 'xpack.ml.settings.filterLists.editFilterList.duplicatedItemsInFilterListWarningMessage', + { + defaultMessage: + 'The following items were already in the filter list: {alreadyInFilter}', + values: { + alreadyInFilter, + }, + } + ) + ); + } - return { - matchingItems, - activePage, - searchQuery: query, - }; - }); - }; + const matchingItems = getMatchingFilterItems(searchQuery, items); + const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - setItemSelected = (item, isSelected) => { - this.setState(prevState => { - const selectedItems = [...prevState.selectedItems]; - const index = selectedItems.indexOf(item); - if (isSelected === true && index === -1) { - selectedItems.push(item); - } else if (isSelected === false && index !== -1) { - selectedItems.splice(index, 1); + return { + items, + matchingItems, + activePage, + searchQuery, + }; + }); + }; + + deleteSelectedItems = () => { + this.setState(prevState => { + const { selectedItems, itemsPerPage, searchQuery } = prevState; + const items = [...prevState.items]; + selectedItems.forEach(item => { + const index = items.indexOf(item); + if (index !== -1) { + items.splice(index, 1); } - - return { - selectedItems, - }; }); - }; - setActivePage = activePage => { - this.setState({ activePage }); - }; + const matchingItems = getMatchingFilterItems(searchQuery, items); + const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); - setItemsPerPage = itemsPerPage => { - this.setState({ - itemsPerPage, - activePage: 0, - }); - }; + return { + items, + matchingItems, + selectedItems: [], + activePage, + searchQuery, + }; + }); + }; - save = () => { - this.setState({ saveInProgress: true }); - - const { loadedFilter, newFilterId, description, items } = this.state; - const { intl } = this.props; - const filterId = this.props.filterId !== undefined ? this.props.filterId : newFilterId; - saveFilterList(filterId, description, items, loadedFilter) - .then(savedFilter => { - this.setLoadedFilterState(savedFilter); - returnToFiltersList(); - }) - .catch(resp => { - console.log(`Error saving filter ${filterId}:`, resp); - toastNotifications.addDanger( - intl.formatMessage( - { - id: 'xpack.ml.settings.filterLists.editFilterList.savingFilterErrorMessage', - defaultMessage: 'An error occurred saving filter {filterId}', - }, - { - filterId, - } - ) - ); - this.setState({ saveInProgress: false }); - }); - }; + onSearchChange = ({ query }) => { + this.setState(prevState => { + const { items, itemsPerPage } = prevState; - render() { - const { - loadedFilter, - newFilterId, - isNewFilterIdInvalid, - description, - items, + const matchingItems = getMatchingFilterItems(query, items); + const activePage = getActivePage(prevState.activePage, itemsPerPage, matchingItems.length); + + return { matchingItems, - selectedItems, - itemsPerPage, activePage, - saveInProgress, - } = this.state; - const { canCreateFilter, canDeleteFilter } = this.props; - - const totalItemCount = items !== undefined ? items.length : 0; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } + searchQuery: query, + }; + }); + }; + + setItemSelected = (item, isSelected) => { + this.setState(prevState => { + const selectedItems = [...prevState.selectedItems]; + const index = selectedItems.indexOf(item); + if (isSelected === true && index === -1) { + selectedItems.push(item); + } else if (isSelected === false && index !== -1) { + selectedItems.splice(index, 1); + } + + return { + selectedItems, + }; + }); + }; + + setActivePage = activePage => { + this.setState({ activePage }); + }; + + setItemsPerPage = itemsPerPage => { + this.setState({ + itemsPerPage, + activePage: 0, + }); + }; + + save = () => { + this.setState({ saveInProgress: true }); + + const { loadedFilter, newFilterId, description, items } = this.state; + const filterId = this.props.filterId !== undefined ? this.props.filterId : newFilterId; + saveFilterList(filterId, description, items, loadedFilter) + .then(savedFilter => { + this.setLoadedFilterState(savedFilter); + returnToFiltersList(); + }) + .catch(resp => { + console.log(`Error saving filter ${filterId}:`, resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.settings.filterLists.editFilterList.savingFilterErrorMessage', { + defaultMessage: 'An error occurred saving filter {filterId}', + values: { + filterId, + }, + }) + ); + this.setState({ saveInProgress: false }); + }); + }; + + render() { + const { + loadedFilter, + newFilterId, + isNewFilterIdInvalid, + description, + items, + matchingItems, + selectedItems, + itemsPerPage, + activePage, + saveInProgress, + } = this.state; + const { canCreateFilter, canDeleteFilter } = this.props; + + const totalItemCount = items !== undefined ? items.length : 0; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); } -); +} +export const EditFilterList = withKibana(EditFilterListUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js index 6ca29ab3f35f2..508fd7972da00 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js @@ -36,6 +36,12 @@ jest.mock('../../../services/ml_api_service', () => ({ }, })); +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: node => { + return node; + }, +})); + import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; @@ -47,7 +53,7 @@ const props = { }; function prepareEditTest() { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); // Cannot find a way to generate the snapshot after the Promise in the mock ml.filters // has resolved. @@ -62,7 +68,7 @@ function prepareEditTest() { describe('EditFilterList', () => { test('renders the edit page for a new filter list and updates ID', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); const instance = wrapper.instance(); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js index 86a2235fcfef0..f1efa173178f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js @@ -23,12 +23,13 @@ import { EuiTitle, } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EditDescriptionPopover } from '../components/edit_description_popover'; import { FilterListUsagePopover } from '../components/filter_list_usage_popover'; -export const EditFilterListHeader = injectI18n(function({ +export const EditFilterListHeader = ({ canCreateFilter, filterId, totalItemCount, @@ -38,8 +39,7 @@ export const EditFilterListHeader = injectI18n(function({ isNewFilterIdInvalid, updateNewFilterId, usedBy, - intl, -}) { +}) => { const title = filterId !== undefined ? ( ); -}); +}; -EditFilterListHeader.WrappedComponent.propTypes = { +EditFilterListHeader.propTypes = { canCreateFilter: PropTypes.bool.isRequired, filterId: PropTypes.string, newFilterId: PropTypes.string, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js index acd2ed88cbecc..b23b1eedf172a 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js @@ -28,7 +28,7 @@ describe('EditFilterListHeader', () => { totalItemCount: 0, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); @@ -42,7 +42,7 @@ describe('EditFilterListHeader', () => { totalItemCount: 15, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); @@ -54,7 +54,7 @@ describe('EditFilterListHeader', () => { totalItemCount: 0, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); @@ -71,7 +71,7 @@ describe('EditFilterListHeader', () => { }, }; - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js index 1995b66c23326..c82be4cbfa71e 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../../util/dependency_cache'; import { isJobIdValid } from '../../../../../common/util/job_utils'; import { ml } from '../../../services/ml_api_service'; @@ -68,6 +68,7 @@ export function addFilterList(filterId, description, items) { reject(error); }); } else { + const toastNotifications = getToastNotifications(); toastNotifications.addDanger(filterWithIdExistsErrorMessage); reject(new Error(filterWithIdExistsErrorMessage)); } diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap index 52971bfe49cd9..5f0cc22fce8b0 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap @@ -14,7 +14,7 @@ exports[`Filter Lists renders a list of filters 1`] = ` horizontalPosition="center" verticalPosition="center" > - diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap index 77936b16667b1..ee9014f752b0c 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap @@ -1,112 +1,127 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Filter Lists Header renders header 1`] = ` - - - - - - -

- -

-
-
- - -

- -

-
-
-
-
- - - - - - - - - -
- - -

- - , - "learnMoreLink": - - , - } - } - /> - -

-
- -
+ `; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js index 949dfe82d9f54..90c65adaaef02 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js @@ -13,106 +13,106 @@ import { PropTypes } from 'prop-types'; import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; - -import { toastNotifications } from 'ui/notify'; +import { i18n } from '@kbn/i18n'; import { NavigationMenu } from '../../../components/navigation_menu'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { FilterListsHeader } from './header'; import { FilterListsTable } from './table'; import { ml } from '../../../services/ml_api_service'; -export const FilterLists = injectI18n( - class extends Component { - static displayName = 'FilterLists'; - static propTypes = { - canCreateFilter: PropTypes.bool.isRequired, - canDeleteFilter: PropTypes.bool.isRequired, - }; +export class FilterListsUI extends Component { + static displayName = 'FilterLists'; + static propTypes = { + canCreateFilter: PropTypes.bool.isRequired, + canDeleteFilter: PropTypes.bool.isRequired, + }; - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - filterLists: [], - selectedFilterLists: [], - }; - } - - componentDidMount() { - this.refreshFilterLists(); - } - - setFilterLists = filterLists => { - // Check selected filter lists still exist. - this.setState(prevState => { - const loadedFilterIds = filterLists.map(filterList => filterList.filter_id); - const selectedFilterLists = prevState.selectedFilterLists.filter(filterList => { - return loadedFilterIds.indexOf(filterList.filter_id) !== -1; - }); - - return { - filterLists, - selectedFilterLists, - }; - }); + this.state = { + filterLists: [], + selectedFilterLists: [], }; + } - setSelectedFilterLists = selectedFilterLists => { - this.setState({ selectedFilterLists }); - }; + componentDidMount() { + this.refreshFilterLists(); + } - refreshFilterLists = () => { - const { intl } = this.props; - // Load the list of filters. - ml.filters - .filtersStats() - .then(filterLists => { - this.setFilterLists(filterLists); - }) - .catch(resp => { - console.log('Error loading list of filters:', resp); - toastNotifications.addDanger( - intl.formatMessage({ - id: 'xpack.ml.settings.filterLists.filterLists.loadingFilterListsErrorMessage', - defaultMessage: 'An error occurred loading the filter lists', - }) - ); - }); - }; + setFilterLists = filterLists => { + // Check selected filter lists still exist. + this.setState(prevState => { + const loadedFilterIds = filterLists.map(filterList => filterList.filter_id); + const selectedFilterLists = prevState.selectedFilterLists.filter(filterList => { + return loadedFilterIds.indexOf(filterList.filter_id) !== -1; + }); - render() { - const { filterLists, selectedFilterLists } = this.state; - const { canCreateFilter, canDeleteFilter } = this.props; - - return ( - - - - - - - - - - - - ); - } + return { + filterLists, + selectedFilterLists, + }; + }); + }; + + setSelectedFilterLists = selectedFilterLists => { + this.setState({ selectedFilterLists }); + }; + + refreshFilterLists = () => { + // Load the list of filters. + ml.filters + .filtersStats() + .then(filterLists => { + this.setFilterLists(filterLists); + }) + .catch(resp => { + console.log('Error loading list of filters:', resp); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate( + 'xpack.ml.settings.filterLists.filterLists.loadingFilterListsErrorMessage', + { + defaultMessage: 'An error occurred loading the filter lists', + } + ) + ); + }); + }; + + render() { + const { filterLists, selectedFilterLists } = this.state; + const { canCreateFilter, canDeleteFilter } = this.props; + + return ( + + + + + + + + + + + + ); } -); +} +export const FilterLists = withKibana(FilterListsUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js index b7be6f1954066..ac9b6e8eb8e7f 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js @@ -16,6 +16,12 @@ jest.mock('../../../privilege/check_privilege', () => ({ checkPermission: () => true, })); +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: node => { + return node; + }, +})); + // Mock the call for loading the list of filters. // The mock is hoisted to the top, so need to prefix the filter variable // with 'mock' so it can be used lazily. @@ -42,7 +48,7 @@ const props = { describe('Filter Lists', () => { test('renders a list of filters', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); // Cannot find a way to generate the snapshot after the Promise in the mock ml.filters // has resolved. diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js index ae0c2ef4338ec..b6ad0e0aec49d 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js @@ -23,12 +23,11 @@ import { EuiButtonEmpty, } from '@elastic/eui'; -import { metadata } from 'ui/metadata'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -// metadata.branch corresponds to the version used in documentation links. -const docsUrl = `https://www.elastic.co/guide/en/machine-learning/${metadata.branch}/ml-rules.html`; - -export function FilterListsHeader({ totalCount, refreshFilterLists }) { +function FilterListsHeaderUI({ totalCount, refreshFilterLists, kibana }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = kibana.services.docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-rules.html`; return ( @@ -99,7 +98,9 @@ You can use the same filter list in multiple jobs.{br}{learnMoreLink}" ); } -FilterListsHeader.propTypes = { +FilterListsHeaderUI.propTypes = { totalCount: PropTypes.number.isRequired, refreshFilterLists: PropTypes.func.isRequired, }; + +export const FilterListsHeader = withKibana(FilterListsHeaderUI); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js index 0d1ca66de5775..fcbf90ec62d4a 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js @@ -22,12 +22,12 @@ import { EuiText, } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DeleteFilterListModal } from '../components/delete_filter_list_modal'; -const UsedByIcon = injectI18n(function({ usedBy, intl }) { +function UsedByIcon({ usedBy }) { // Renders a tick or cross in the 'usedBy' column to indicate whether // the filter list is in use in a detectors in any jobs. let icon; @@ -35,8 +35,7 @@ const UsedByIcon = injectI18n(function({ usedBy, intl }) { icon = ( @@ -45,8 +44,7 @@ const UsedByIcon = injectI18n(function({ usedBy, intl }) { icon = ( @@ -54,9 +52,9 @@ const UsedByIcon = injectI18n(function({ usedBy, intl }) { } return icon; -}); +} -UsedByIcon.WrappedComponent.propTypes = { +UsedByIcon.propTypes = { usedBy: PropTypes.object, }; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js b/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js index 8efe558fda961..6b4e752845774 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js @@ -9,7 +9,6 @@ import React from 'react'; import { Settings } from './settings'; -jest.mock('../contexts/ui/use_ui_chrome_context'); jest.mock('../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 9aafab12a7156..2084998136460 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -15,8 +15,6 @@ import React, { Component } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; -import { timefilter } from 'ui/timefilter'; - // don't use something like plugins/ml/../common // because it won't work with the jest tests import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../../common/constants/states'; @@ -28,7 +26,9 @@ import { PROGRESS_STATES } from './progress_states'; import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; import { mlForecastService } from '../../../services/forecast_service'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; export const FORECAST_DURATION_MAX_DAYS = 3650; // Max forecast duration allowed by analytics. @@ -54,483 +54,486 @@ function getDefaultState() { }; } -export const ForecastingModal = injectI18n( - class ForecastingModal extends Component { - static propTypes = { - isDisabled: PropTypes.bool, - job: PropTypes.object, - detectorIndex: PropTypes.number, - entities: PropTypes.array, - setForecastId: PropTypes.func, - }; - - constructor(props) { - super(props); - this.state = getDefaultState(); - - // Used to poll for updates on a running forecast. - this.forecastChecker = null; - } +export class ForecastingModalUI extends Component { + static propTypes = { + isDisabled: PropTypes.bool, + job: PropTypes.object, + detectorIndex: PropTypes.number, + entities: PropTypes.array, + setForecastId: PropTypes.func, + }; + + constructor(props) { + super(props); + this.state = getDefaultState(); + + // Used to poll for updates on a running forecast. + this.forecastChecker = null; + } - addMessage = (message, status, clearFirst = false) => { - const msg = { message, status }; - - this.setState(prevState => ({ - messages: clearFirst ? [msg] : [...prevState.messages, msg], - })); - }; - - viewForecast = forecastId => { - this.props.setForecastId(forecastId); - this.closeModal(); - }; - - onNewForecastDurationChange = event => { - const { intl } = this.props; - const newForecastDurationErrors = []; - let isNewForecastDurationValid = true; - const duration = parseInterval(event.target.value); - if (duration === null) { - isNewForecastDurationValid = false; - newForecastDurationErrors.push( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.forecastingModal.invalidDurationFormatErrorMessage', + addMessage = (message, status, clearFirst = false) => { + const msg = { message, status }; + + this.setState(prevState => ({ + messages: clearFirst ? [msg] : [...prevState.messages, msg], + })); + }; + + viewForecast = forecastId => { + this.props.setForecastId(forecastId); + this.closeModal(); + }; + + onNewForecastDurationChange = event => { + const newForecastDurationErrors = []; + let isNewForecastDurationValid = true; + const duration = parseInterval(event.target.value); + if (duration === null) { + isNewForecastDurationValid = false; + newForecastDurationErrors.push( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.invalidDurationFormatErrorMessage', + { defaultMessage: 'Invalid duration format', - }) - ); - } else if (duration.asMilliseconds() > FORECAST_DURATION_MAX_MS) { - isNewForecastDurationValid = false; - newForecastDurationErrors.push( - intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeGreaterThanMaximumErrorMessage', - defaultMessage: - 'Forecast duration must not be greater than {maximumForecastDurationDays} days', - }, - { maximumForecastDurationDays: FORECAST_DURATION_MAX_DAYS } - ) - ); - } else if (duration.asMilliseconds() === 0) { - isNewForecastDurationValid = false; - newForecastDurationErrors.push( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeZeroErrorMessage', + } + ) + ); + } else if (duration.asMilliseconds() > FORECAST_DURATION_MAX_MS) { + isNewForecastDurationValid = false; + newForecastDurationErrors.push( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeGreaterThanMaximumErrorMessage', + { + defaultMessage: + 'Forecast duration must not be greater than {maximumForecastDurationDays} days', + values: { maximumForecastDurationDays: FORECAST_DURATION_MAX_DAYS }, + } + ) + ); + } else if (duration.asMilliseconds() === 0) { + isNewForecastDurationValid = false; + newForecastDurationErrors.push( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeZeroErrorMessage', + { defaultMessage: 'Forecast duration must not be zero', - }) - ); - } - - this.setState({ - newForecastDuration: event.target.value, - isNewForecastDurationValid, - newForecastDurationErrors, - }); - }; + } + ) + ); + } - checkJobStateAndRunForecast = () => { - this.setState({ - isForecastRequested: true, - messages: [], - }); + this.setState({ + newForecastDuration: event.target.value, + isNewForecastDurationValid, + newForecastDurationErrors, + }); + }; - // A forecast can only be run on an opened job, - // so open job if it is closed. - if (this.props.job.state === JOB_STATE.CLOSED) { - this.openJobAndRunForecast(); - } else { - this.runForecast(false); - } - }; + checkJobStateAndRunForecast = () => { + this.setState({ + isForecastRequested: true, + messages: [], + }); + + // A forecast can only be run on an opened job, + // so open job if it is closed. + if (this.props.job.state === JOB_STATE.CLOSED) { + this.openJobAndRunForecast(); + } else { + this.runForecast(false); + } + }; - openJobAndRunForecast = () => { - // Opens a job in a 'closed' state prior to running a forecast. - this.setState({ - jobOpeningState: PROGRESS_STATES.WAITING, + openJobAndRunForecast = () => { + // Opens a job in a 'closed' state prior to running a forecast. + this.setState({ + jobOpeningState: PROGRESS_STATES.WAITING, + }); + + mlJobService + .openJob(this.props.job.job_id) + .then(() => { + // If open was successful run the forecast, then close the job again. + this.setState({ + jobOpeningState: PROGRESS_STATES.DONE, + }); + this.runForecast(true); + }) + .catch(resp => { + console.log('Time series forecast modal - could not open job:', resp); + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithOpeningJobBeforeRunningForecastErrorMessage', + { + defaultMessage: 'Error opening job before running forecast', + } + ), + MESSAGE_LEVEL.ERROR + ); + this.setState({ + jobOpeningState: PROGRESS_STATES.ERROR, + }); }); + }; + + runForecastErrorHandler = (resp, closeJob) => { + this.setState({ forecastProgress: PROGRESS_STATES.ERROR }); + console.log('Time series forecast modal - error running forecast:', resp); + if (resp && resp.message) { + this.addMessage(resp.message, MESSAGE_LEVEL.ERROR, true); + } else { + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.unexpectedResponseFromRunningForecastErrorMessage', + { + defaultMessage: + 'Unexpected response from running forecast. The request may have failed.', + } + ), + MESSAGE_LEVEL.ERROR, + true + ); + } + if (closeJob === true) { + this.setState({ jobClosingState: PROGRESS_STATES.WAITING }); mlJobService - .openJob(this.props.job.job_id) + .closeJob(this.props.job.job_id) .then(() => { - // If open was successful run the forecast, then close the job again. - this.setState({ - jobOpeningState: PROGRESS_STATES.DONE, - }); - this.runForecast(true); + this.setState({ jobClosingState: PROGRESS_STATES.DONE }); }) - .catch(resp => { - console.log('Time series forecast modal - could not open job:', resp); + .catch(response => { + console.log('Time series forecast modal - could not close job:', response); this.addMessage( - this.props.intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithOpeningJobBeforeRunningForecastErrorMessage', - defaultMessage: 'Error opening job before running forecast', - }), + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobErrorMessage', + { + defaultMessage: 'Error closing job', + } + ), MESSAGE_LEVEL.ERROR ); - this.setState({ - jobOpeningState: PROGRESS_STATES.ERROR, - }); + this.setState({ jobClosingState: PROGRESS_STATES.ERROR }); }); - }; - - runForecastErrorHandler = (resp, closeJob) => { - const intl = this.props.intl; - - this.setState({ forecastProgress: PROGRESS_STATES.ERROR }); - console.log('Time series forecast modal - error running forecast:', resp); - if (resp && resp.message) { - this.addMessage(resp.message, MESSAGE_LEVEL.ERROR, true); - } else { - this.addMessage( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.unexpectedResponseFromRunningForecastErrorMessage', - defaultMessage: - 'Unexpected response from running forecast. The request may have failed.', - }), - MESSAGE_LEVEL.ERROR, - true - ); - } - - if (closeJob === true) { - this.setState({ jobClosingState: PROGRESS_STATES.WAITING }); - mlJobService - .closeJob(this.props.job.job_id) - .then(() => { - this.setState({ jobClosingState: PROGRESS_STATES.DONE }); - }) - .catch(response => { - console.log('Time series forecast modal - could not close job:', response); - this.addMessage( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobErrorMessage', - defaultMessage: 'Error closing job', - }), - MESSAGE_LEVEL.ERROR - ); - this.setState({ jobClosingState: PROGRESS_STATES.ERROR }); - }); - } - }; - - runForecast = closeJobAfterRunning => { - this.setState({ - forecastProgress: 0, - }); + } + }; - // Always supply the duration to the endpoint in seconds as some of the moment duration - // formats accepted by Kibana (w, M, y) are not valid formats in Elasticsearch. - const durationInSeconds = parseInterval(this.state.newForecastDuration).asSeconds(); + runForecast = closeJobAfterRunning => { + this.setState({ + forecastProgress: 0, + }); + + // Always supply the duration to the endpoint in seconds as some of the moment duration + // formats accepted by Kibana (w, M, y) are not valid formats in Elasticsearch. + const durationInSeconds = parseInterval(this.state.newForecastDuration).asSeconds(); + + mlForecastService + .runForecast(this.props.job.job_id, `${durationInSeconds}s`) + .then(resp => { + // Endpoint will return { acknowledged:true, id: } before forecast is complete. + // So wait for results and then refresh the dashboard to the end of the forecast. + if (resp.forecast_id !== undefined) { + this.waitForForecastResults(resp.forecast_id, closeJobAfterRunning); + } else { + this.runForecastErrorHandler(resp, closeJobAfterRunning); + } + }) + .catch(resp => this.runForecastErrorHandler(resp, closeJobAfterRunning)); + }; + waitForForecastResults = (forecastId, closeJobAfterRunning) => { + // Obtain the stats for the forecast request and check forecast is progressing. + // When the stats show the forecast is finished, load the + // forecast results into the view. + let previousProgress = 0; + let noProgressMs = 0; + this.forecastChecker = setInterval(() => { mlForecastService - .runForecast(this.props.job.job_id, `${durationInSeconds}s`) + .getForecastRequestStats(this.props.job, forecastId) .then(resp => { - // Endpoint will return { acknowledged:true, id: } before forecast is complete. - // So wait for results and then refresh the dashboard to the end of the forecast. - if (resp.forecast_id !== undefined) { - this.waitForForecastResults(resp.forecast_id, closeJobAfterRunning); - } else { - this.runForecastErrorHandler(resp, closeJobAfterRunning); + // Get the progress (stats value is between 0 and 1). + const progress = _.get(resp, ['stats', 'forecast_progress'], previousProgress); + const status = _.get(resp, ['stats', 'forecast_status']); + + // The requests for forecast stats can get routed to different shards, + // and if these operate at different speeds there is a chance that a + // previous request could arrive later. + // The progress reported by the back-end should never go down, so + // to be on the safe side, only update state if progress has increased. + if (progress > previousProgress) { + this.setState({ forecastProgress: Math.round(100 * progress) }); } - }) - .catch(resp => this.runForecastErrorHandler(resp, closeJobAfterRunning)); - }; - - waitForForecastResults = (forecastId, closeJobAfterRunning) => { - // Obtain the stats for the forecast request and check forecast is progressing. - // When the stats show the forecast is finished, load the - // forecast results into the view. - const { intl } = this.props; - let previousProgress = 0; - let noProgressMs = 0; - this.forecastChecker = setInterval(() => { - mlForecastService - .getForecastRequestStats(this.props.job, forecastId) - .then(resp => { - // Get the progress (stats value is between 0 and 1). - const progress = _.get(resp, ['stats', 'forecast_progress'], previousProgress); - const status = _.get(resp, ['stats', 'forecast_status']); - - // The requests for forecast stats can get routed to different shards, - // and if these operate at different speeds there is a chance that a - // previous request could arrive later. - // The progress reported by the back-end should never go down, so - // to be on the safe side, only update state if progress has increased. - if (progress > previousProgress) { - this.setState({ forecastProgress: Math.round(100 * progress) }); - } - // Display any messages returned in the request stats. - let messages = _.get(resp, ['stats', 'forecast_messages'], []); - messages = messages.map(message => ({ message, status: MESSAGE_LEVEL.WARNING })); - this.setState({ messages }); - - if (status === FORECAST_REQUEST_STATE.FINISHED) { - clearInterval(this.forecastChecker); - - if (closeJobAfterRunning === true) { - this.setState({ jobClosingState: PROGRESS_STATES.WAITING }); - mlJobService - .closeJob(this.props.job.job_id) - .then(() => { - this.setState({ - jobClosingState: PROGRESS_STATES.DONE, - }); - this.props.setForecastId(forecastId); - this.closeAfterRunningForecast(); - }) - .catch(response => { - // Load the forecast data in the main page, - // but leave this dialog open so the error can be viewed. - console.log('Time series forecast modal - could not close job:', response); - this.addMessage( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobAfterRunningForecastErrorMessage', - defaultMessage: 'Error closing job after running forecast', - }), - MESSAGE_LEVEL.ERROR - ); - this.setState({ - jobClosingState: PROGRESS_STATES.ERROR, - }); - this.props.setForecastId(forecastId); + // Display any messages returned in the request stats. + let messages = _.get(resp, ['stats', 'forecast_messages'], []); + messages = messages.map(message => ({ message, status: MESSAGE_LEVEL.WARNING })); + this.setState({ messages }); + + if (status === FORECAST_REQUEST_STATE.FINISHED) { + clearInterval(this.forecastChecker); + + if (closeJobAfterRunning === true) { + this.setState({ jobClosingState: PROGRESS_STATES.WAITING }); + mlJobService + .closeJob(this.props.job.job_id) + .then(() => { + this.setState({ + jobClosingState: PROGRESS_STATES.DONE, }); - } else { - this.props.setForecastId(forecastId); - this.closeAfterRunningForecast(); - } - } else { - // Display a warning and abort check if the forecast hasn't - // progressed for WARN_NO_PROGRESS_MS. - if (progress === previousProgress) { - noProgressMs += FORECAST_STATS_POLL_FREQUENCY; - if (noProgressMs > WARN_NO_PROGRESS_MS) { - console.log( - `Forecast request has not progressed for ${WARN_NO_PROGRESS_MS}ms. Cancelling check.` - ); + this.props.setForecastId(forecastId); + this.closeAfterRunningForecast(); + }) + .catch(response => { + // Load the forecast data in the main page, + // but leave this dialog open so the error can be viewed. + console.log('Time series forecast modal - could not close job:', response); this.addMessage( - intl.formatMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobAfterRunningForecastErrorMessage', { - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.noProgressReportedForNewForecastErrorMessage', - defaultMessage: - 'No progress reported for the new forecast for {WarnNoProgressMs}ms.' + - 'An error may have occurred whilst running the forecast.', - }, - { WarnNoProgressMs: WARN_NO_PROGRESS_MS } + defaultMessage: 'Error closing job after running forecast', + } ), MESSAGE_LEVEL.ERROR ); - - // Try and load any results which may have been created. + this.setState({ + jobClosingState: PROGRESS_STATES.ERROR, + }); this.props.setForecastId(forecastId); - this.setState({ forecastProgress: PROGRESS_STATES.ERROR }); - clearInterval(this.forecastChecker); - } - } else { - if (progress > previousProgress) { - previousProgress = progress; - } - - // Reset the 'no progress' check value. - noProgressMs = 0; + }); + } else { + this.props.setForecastId(forecastId); + this.closeAfterRunningForecast(); + } + } else { + // Display a warning and abort check if the forecast hasn't + // progressed for WARN_NO_PROGRESS_MS. + if (progress === previousProgress) { + noProgressMs += FORECAST_STATS_POLL_FREQUENCY; + if (noProgressMs > WARN_NO_PROGRESS_MS) { + console.log( + `Forecast request has not progressed for ${WARN_NO_PROGRESS_MS}ms. Cancelling check.` + ); + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.noProgressReportedForNewForecastErrorMessage', + { + defaultMessage: + 'No progress reported for the new forecast for {WarnNoProgressMs}ms.' + + 'An error may have occurred whilst running the forecast.', + values: { WarnNoProgressMs: WARN_NO_PROGRESS_MS }, + } + ), + MESSAGE_LEVEL.ERROR + ); + + // Try and load any results which may have been created. + this.props.setForecastId(forecastId); + this.setState({ forecastProgress: PROGRESS_STATES.ERROR }); + clearInterval(this.forecastChecker); + } + } else { + if (progress > previousProgress) { + previousProgress = progress; } + + // Reset the 'no progress' check value. + noProgressMs = 0; } - }) - .catch(resp => { - console.log( - 'Time series forecast modal - error loading stats of forecast from elasticsearch:', - resp - ); - this.addMessage( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithLoadingStatsOfRunningForecastErrorMessage', + } + }) + .catch(resp => { + console.log( + 'Time series forecast modal - error loading stats of forecast from elasticsearch:', + resp + ); + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithLoadingStatsOfRunningForecastErrorMessage', + { defaultMessage: 'Error loading stats of running forecast.', - }), - MESSAGE_LEVEL.ERROR - ); - this.setState({ - forecastProgress: PROGRESS_STATES.ERROR, - }); - clearInterval(this.forecastChecker); + } + ), + MESSAGE_LEVEL.ERROR + ); + this.setState({ + forecastProgress: PROGRESS_STATES.ERROR, + }); + clearInterval(this.forecastChecker); + }); + }, FORECAST_STATS_POLL_FREQUENCY); + }; + + openModal = () => { + const job = this.props.job; + + if (typeof job === 'object') { + // Get the list of all the finished forecasts for this job with results at or later than the dashboard 'from' time. + const { timefilter } = this.props.kibana.services.data.query.timefilter; + const bounds = timefilter.getActiveBounds(); + const statusFinishedQuery = { + term: { + forecast_status: FORECAST_REQUEST_STATE.FINISHED, + }, + }; + mlForecastService + .getForecastsSummary(job, statusFinishedQuery, bounds.min.valueOf(), FORECASTS_VIEW_MAX) + .then(resp => { + this.setState({ + previousForecasts: resp.forecasts, }); - }, FORECAST_STATS_POLL_FREQUENCY); - }; - - openModal = () => { - const { intl } = this.props; - const job = this.props.job; - - if (typeof job === 'object') { - // Get the list of all the finished forecasts for this job with results at or later than the dashboard 'from' time. - const bounds = timefilter.getActiveBounds(); - const statusFinishedQuery = { - term: { - forecast_status: FORECAST_REQUEST_STATE.FINISHED, - }, - }; - mlForecastService - .getForecastsSummary(job, statusFinishedQuery, bounds.min.valueOf(), FORECASTS_VIEW_MAX) - .then(resp => { - this.setState({ - previousForecasts: resp.forecasts, + }) + .catch(resp => { + console.log('Time series forecast modal - error obtaining forecasts summary:', resp); + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithObtainingListOfPreviousForecastsErrorMessage', + { + defaultMessage: 'Error obtaining list of previous forecasts', + } + ), + MESSAGE_LEVEL.ERROR + ); + }); + + // Display a warning about running a forecast if there is high number + // of partitioning fields. + const entityFieldNames = this.props.entities.map(entity => entity.fieldName); + if (entityFieldNames.length > 0) { + ml.getCardinalityOfFields({ + index: job.datafeed_config.indices, + fieldNames: entityFieldNames, + query: job.datafeed_config.query, + timeFieldName: job.data_description.time_field, + earliestMs: job.data_counts.earliest_record_timestamp, + latestMs: job.data_counts.latest_record_timestamp, + }) + .then(results => { + let numPartitions = 1; + Object.values(results).forEach(cardinality => { + numPartitions = numPartitions * cardinality; }); + if (numPartitions > WARN_NUM_PARTITIONS) { + this.addMessage( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.dataContainsMorePartitionsMessage', + { + defaultMessage: + 'Note that this data contains more than {warnNumPartitions} ' + + 'partitions so running a forecast may take a long time and consume a high amount of resource', + values: { warnNumPartitions: WARN_NUM_PARTITIONS }, + } + ), + MESSAGE_LEVEL.WARNING + ); + } }) .catch(resp => { - console.log('Time series forecast modal - error obtaining forecasts summary:', resp); - this.addMessage( - intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithObtainingListOfPreviousForecastsErrorMessage', - defaultMessage: 'Error obtaining list of previous forecasts', - }), - MESSAGE_LEVEL.ERROR + console.log( + 'Time series forecast modal - error obtaining cardinality of fields:', + resp ); }); + } - // Display a warning about running a forecast if there is high number - // of partitioning fields. - const entityFieldNames = this.props.entities.map(entity => entity.fieldName); - if (entityFieldNames.length > 0) { - ml.getCardinalityOfFields({ - index: job.datafeed_config.indices, - fieldNames: entityFieldNames, - query: job.datafeed_config.query, - timeFieldName: job.data_description.time_field, - earliestMs: job.data_counts.earliest_record_timestamp, - latestMs: job.data_counts.latest_record_timestamp, - }) - .then(results => { - let numPartitions = 1; - Object.values(results).forEach(cardinality => { - numPartitions = numPartitions * cardinality; - }); - if (numPartitions > WARN_NUM_PARTITIONS) { - this.addMessage( - intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.dataContainsMorePartitionsMessage', - defaultMessage: - 'Note that this data contains more than {warnNumPartitions} ' + - 'partitions so running a forecast may take a long time and consume a high amount of resource', - }, - { warnNumPartitions: WARN_NUM_PARTITIONS } - ), - MESSAGE_LEVEL.WARNING - ); - } - }) - .catch(resp => { - console.log( - 'Time series forecast modal - error obtaining cardinality of fields:', - resp - ); - }); - } + this.setState({ isModalVisible: true }); + } + }; - this.setState({ isModalVisible: true }); - } - }; - - closeAfterRunningForecast = () => { - // Only close the dialog automatically after a forecast has run - // if the message bar is clear. Otherwise the user may not catch - // any messages returned in the forecast request stats. - if (this.state.messages.length === 0) { - // Wrap the close in a timeout to give the user a chance to see progress update. - setTimeout(() => { - this.closeModal(); - }, 1000); - } - }; + closeAfterRunningForecast = () => { + // Only close the dialog automatically after a forecast has run + // if the message bar is clear. Otherwise the user may not catch + // any messages returned in the forecast request stats. + if (this.state.messages.length === 0) { + // Wrap the close in a timeout to give the user a chance to see progress update. + setTimeout(() => { + this.closeModal(); + }, 1000); + } + }; - closeModal = () => { - if (this.forecastChecker !== null) { - clearInterval(this.forecastChecker); - } - this.setState(getDefaultState()); - }; - - render() { - // Forecasting disabled if detector has an over field or job created < 6.1.0. - let isForecastingDisabled = false; - let forecastingDisabledMessage = null; - const { intl, job } = this.props; - if (job !== undefined) { - const detector = job.analysis_config.detectors[this.props.detectorIndex]; - const overFieldName = detector.over_field_name; - if (overFieldName !== undefined) { - isForecastingDisabled = true; - forecastingDisabledMessage = intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingNotAvailableForPopulationDetectorsMessage', + closeModal = () => { + if (this.forecastChecker !== null) { + clearInterval(this.forecastChecker); + } + this.setState(getDefaultState()); + }; + + render() { + // Forecasting disabled if detector has an over field or job created < 6.1.0. + let isForecastingDisabled = false; + let forecastingDisabledMessage = null; + const { job } = this.props; + if (job !== undefined) { + const detector = job.analysis_config.detectors[this.props.detectorIndex]; + const overFieldName = detector.over_field_name; + if (overFieldName !== undefined) { + isForecastingDisabled = true; + forecastingDisabledMessage = i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingNotAvailableForPopulationDetectorsMessage', + { defaultMessage: 'Forecasting is not available for population detectors with an over field', - }); - } else if (isJobVersionGte(job, FORECAST_JOB_MIN_VERSION) === false) { - isForecastingDisabled = true; - forecastingDisabledMessage = intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingOnlyAvailableForJobsCreatedInSpecifiedVersionMessage', - defaultMessage: - 'Forecasting is only available for jobs created in version {minVersion} or later', - }, - { minVersion: FORECAST_JOB_MIN_VERSION } - ); - } + } + ); + } else if (isJobVersionGte(job, FORECAST_JOB_MIN_VERSION) === false) { + isForecastingDisabled = true; + forecastingDisabledMessage = i18n.translate( + 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingOnlyAvailableForJobsCreatedInSpecifiedVersionMessage', + { + defaultMessage: + 'Forecasting is only available for jobs created in version {minVersion} or later', + values: { minVersion: FORECAST_JOB_MIN_VERSION }, + } + ); } + } - const forecastButton = ( - - + + + ); + + return ( +
+ {isForecastingDisabled ? ( + + {forecastButton} + + ) : ( + forecastButton + )} + + {this.state.isModalVisible && ( + - - ); - - return ( -
- {isForecastingDisabled ? ( - - {forecastButton} - - ) : ( - forecastButton - )} - - {this.state.isModalVisible && ( - - )} -
- ); - } + )} +
+ ); } -); +} + +export const ForecastingModal = withKibana(ForecastingModalUI); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 2eaa4a907af66..3c639239757db 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -10,14 +10,12 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { Component } from 'react'; import useObservable from 'react-use/lib/useObservable'; import _ from 'lodash'; import d3 from 'd3'; import moment from 'moment'; -import chrome from 'ui/chrome'; - import { getSeverityWithLow, getMultiBucketImpactLabel, @@ -52,7 +50,7 @@ import { unhighlightFocusChartAnnotation, } from './timeseries_chart_annotations'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; const focusZoomPanelHeight = 25; const focusChartHeight = 310; @@ -62,7 +60,6 @@ const contextChartLineTopMargin = 3; const chartSpacing = 25; const swimlaneHeight = 30; const margin = { top: 10, right: 10, bottom: 15, left: 40 }; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); const ZOOM_INTERVAL_OPTIONS = [ { duration: moment.duration(1, 'h'), label: '1h' }, @@ -91,678 +88,765 @@ function getSvgHeight() { ); } -const TimeseriesChartIntl = injectI18n( - class TimeseriesChart extends React.Component { - static propTypes = { - annotation: PropTypes.object, - autoZoomDuration: PropTypes.number, - bounds: PropTypes.object, - contextAggregationInterval: PropTypes.object, - contextChartData: PropTypes.array, - contextForecastData: PropTypes.array, - contextChartSelected: PropTypes.func.isRequired, - detectorIndex: PropTypes.number, - focusAggregationInterval: PropTypes.object, - focusAnnotationData: PropTypes.array, - focusChartData: PropTypes.array, - focusForecastData: PropTypes.array, - modelPlotEnabled: PropTypes.bool.isRequired, - renderFocusChartOnly: PropTypes.bool.isRequired, - selectedJob: PropTypes.object, - showForecast: PropTypes.bool.isRequired, - showModelBounds: PropTypes.bool.isRequired, - svgWidth: PropTypes.number.isRequired, - swimlaneData: PropTypes.array, - zoomFrom: PropTypes.object, - zoomTo: PropTypes.object, - zoomFromFocusLoaded: PropTypes.object, - zoomToFocusLoaded: PropTypes.object, - }; - - rowMouseenterSubscriber = null; - rowMouseleaveSubscriber = null; - - componentWillUnmount() { - const element = d3.select(this.rootNode); - element.html(''); - - if (this.rowMouseenterSubscriber !== null) { - this.rowMouseenterSubscriber.unsubscribe(); - } - if (this.rowMouseleaveSubscriber !== null) { - this.rowMouseleaveSubscriber.unsubscribe(); - } +class TimeseriesChartIntl extends Component { + static propTypes = { + annotation: PropTypes.object, + autoZoomDuration: PropTypes.number, + bounds: PropTypes.object, + contextAggregationInterval: PropTypes.object, + contextChartData: PropTypes.array, + contextForecastData: PropTypes.array, + contextChartSelected: PropTypes.func.isRequired, + detectorIndex: PropTypes.number, + focusAggregationInterval: PropTypes.object, + focusAnnotationData: PropTypes.array, + focusChartData: PropTypes.array, + focusForecastData: PropTypes.array, + modelPlotEnabled: PropTypes.bool.isRequired, + renderFocusChartOnly: PropTypes.bool.isRequired, + selectedJob: PropTypes.object, + showForecast: PropTypes.bool.isRequired, + showModelBounds: PropTypes.bool.isRequired, + svgWidth: PropTypes.number.isRequired, + swimlaneData: PropTypes.array, + zoomFrom: PropTypes.object, + zoomTo: PropTypes.object, + zoomFromFocusLoaded: PropTypes.object, + zoomToFocusLoaded: PropTypes.object, + }; + + rowMouseenterSubscriber = null; + rowMouseleaveSubscriber = null; + + componentWillUnmount() { + const element = d3.select(this.rootNode); + element.html(''); + + if (this.rowMouseenterSubscriber !== null) { + this.rowMouseenterSubscriber.unsubscribe(); } + if (this.rowMouseleaveSubscriber !== null) { + this.rowMouseleaveSubscriber.unsubscribe(); + } + } - componentDidMount() { - const { svgWidth } = this.props; - - this.vizWidth = svgWidth - margin.left - margin.right; - const vizWidth = this.vizWidth; - - this.focusXScale = d3.time.scale().range([0, vizWidth]); - this.focusYScale = d3.scale.linear().range([focusHeight, focusZoomPanelHeight]); - const focusXScale = this.focusXScale; - const focusYScale = this.focusYScale; - - this.focusXAxis = d3.svg - .axis() - .scale(focusXScale) - .orient('bottom') - .innerTickSize(-focusChartHeight) - .outerTickSize(0) - .tickPadding(10); - this.focusYAxis = d3.svg - .axis() - .scale(focusYScale) - .orient('left') - .innerTickSize(-vizWidth) - .outerTickSize(0) - .tickPadding(10); - - this.focusValuesLine = d3.svg - .line() - .x(function(d) { - return focusXScale(d.date); - }) - .y(function(d) { - return focusYScale(d.value); - }) - .defined(d => d.value !== null); - this.focusBoundedArea = d3.svg - .area() - .x(function(d) { - return focusXScale(d.date) || 1; - }) - .y0(function(d) { - return focusYScale(d.upper); - }) - .y1(function(d) { - return focusYScale(d.lower); - }) - .defined(d => d.lower !== null && d.upper !== null); - - this.contextXScale = d3.time.scale().range([0, vizWidth]); - this.contextYScale = d3.scale.linear().range([contextChartHeight, contextChartLineTopMargin]); - - this.fieldFormat = undefined; - - // Annotations Brush - if (mlAnnotationsEnabled) { - this.annotateBrush = getAnnotationBrush.call(this); + componentDidMount() { + const { svgWidth } = this.props; + + this.vizWidth = svgWidth - margin.left - margin.right; + const vizWidth = this.vizWidth; + + this.focusXScale = d3.time.scale().range([0, vizWidth]); + this.focusYScale = d3.scale.linear().range([focusHeight, focusZoomPanelHeight]); + const focusXScale = this.focusXScale; + const focusYScale = this.focusYScale; + + this.focusXAxis = d3.svg + .axis() + .scale(focusXScale) + .orient('bottom') + .innerTickSize(-focusChartHeight) + .outerTickSize(0) + .tickPadding(10); + this.focusYAxis = d3.svg + .axis() + .scale(focusYScale) + .orient('left') + .innerTickSize(-vizWidth) + .outerTickSize(0) + .tickPadding(10); + + this.focusValuesLine = d3.svg + .line() + .x(function(d) { + return focusXScale(d.date); + }) + .y(function(d) { + return focusYScale(d.value); + }) + .defined(d => d.value !== null); + this.focusBoundedArea = d3.svg + .area() + .x(function(d) { + return focusXScale(d.date) || 1; + }) + .y0(function(d) { + return focusYScale(d.upper); + }) + .y1(function(d) { + return focusYScale(d.lower); + }) + .defined(d => d.lower !== null && d.upper !== null); + + this.contextXScale = d3.time.scale().range([0, vizWidth]); + this.contextYScale = d3.scale.linear().range([contextChartHeight, contextChartLineTopMargin]); + + this.fieldFormat = undefined; + + // Annotations Brush + this.annotateBrush = getAnnotationBrush.call(this); + + // brush for focus brushing + this.brush = d3.svg.brush(); + + this.mask = undefined; + + // Listeners for mouseenter/leave events for rows in the table + // to highlight the corresponding anomaly mark in the focus chart. + const highlightFocusChartAnomaly = this.highlightFocusChartAnomaly.bind(this); + const boundHighlightFocusChartAnnotation = highlightFocusChartAnnotation.bind(this); + function tableRecordMousenterListener({ record, type = 'anomaly' }) { + if (type === 'anomaly') { + highlightFocusChartAnomaly(record); + } else if (type === 'annotation') { + boundHighlightFocusChartAnnotation(record); } + } - // brush for focus brushing - this.brush = d3.svg.brush(); - - this.mask = undefined; - - // Listeners for mouseenter/leave events for rows in the table - // to highlight the corresponding anomaly mark in the focus chart. - const highlightFocusChartAnomaly = this.highlightFocusChartAnomaly.bind(this); - const boundHighlightFocusChartAnnotation = highlightFocusChartAnnotation.bind(this); - function tableRecordMousenterListener({ record, type = 'anomaly' }) { - if (type === 'anomaly') { - highlightFocusChartAnomaly(record); - } else if (type === 'annotation') { - boundHighlightFocusChartAnnotation(record); - } + const unhighlightFocusChartAnomaly = this.unhighlightFocusChartAnomaly.bind(this); + const boundUnhighlightFocusChartAnnotation = unhighlightFocusChartAnnotation.bind(this); + function tableRecordMouseleaveListener({ record, type = 'anomaly' }) { + if (type === 'anomaly') { + unhighlightFocusChartAnomaly(record); + } else { + boundUnhighlightFocusChartAnnotation(record); } + } - const unhighlightFocusChartAnomaly = this.unhighlightFocusChartAnomaly.bind(this); - const boundUnhighlightFocusChartAnnotation = unhighlightFocusChartAnnotation.bind(this); - function tableRecordMouseleaveListener({ record, type = 'anomaly' }) { - if (type === 'anomaly') { - unhighlightFocusChartAnomaly(record); - } else { - boundUnhighlightFocusChartAnnotation(record); - } - } + this.rowMouseenterSubscriber = mlTableService.rowMouseenter$.subscribe( + tableRecordMousenterListener + ); + this.rowMouseleaveSubscriber = mlTableService.rowMouseleave$.subscribe( + tableRecordMouseleaveListener + ); - this.rowMouseenterSubscriber = mlTableService.rowMouseenter$.subscribe( - tableRecordMousenterListener - ); - this.rowMouseleaveSubscriber = mlTableService.rowMouseleave$.subscribe( - tableRecordMouseleaveListener - ); + this.renderChart(); + this.drawContextChartSelection(); + this.renderFocusChart(); + } + componentDidUpdate() { + if (this.props.renderFocusChartOnly === false) { this.renderChart(); this.drawContextChartSelection(); - this.renderFocusChart(); } - componentDidUpdate() { - if (this.props.renderFocusChartOnly === false) { - this.renderChart(); - this.drawContextChartSelection(); - } - - this.renderFocusChart(); - - if (mlAnnotationsEnabled && this.props.annotation === null) { - const chartElement = d3.select(this.rootNode); - chartElement.select('g.mlAnnotationBrush').call(this.annotateBrush.extent([0, 0])); - } - } + this.renderFocusChart(); - renderChart() { - const { - contextChartData, - contextForecastData, - detectorIndex, - modelPlotEnabled, - selectedJob, - svgWidth, - } = this.props; - - const createFocusChart = this.createFocusChart.bind(this); - const drawContextElements = this.drawContextElements.bind(this); - const focusXScale = this.focusXScale; - const focusYAxis = this.focusYAxis; - const focusYScale = this.focusYScale; - - const svgHeight = getSvgHeight(); - - // Clear any existing elements from the visualization, - // then build the svg elements for the bubble chart. + if (this.props.annotation === null) { const chartElement = d3.select(this.rootNode); - chartElement.selectAll('*').remove(); - - if (typeof selectedJob !== 'undefined') { - this.fieldFormat = mlFieldFormatService.getFieldFormat(selectedJob.job_id, detectorIndex); - } else { - return; - } + chartElement.select('g.mlAnnotationBrush').call(this.annotateBrush.extent([0, 0])); + } + } - if (contextChartData === undefined) { - return; - } + renderChart() { + const { + contextChartData, + contextForecastData, + detectorIndex, + modelPlotEnabled, + selectedJob, + svgWidth, + } = this.props; + + const createFocusChart = this.createFocusChart.bind(this); + const drawContextElements = this.drawContextElements.bind(this); + const focusXScale = this.focusXScale; + const focusYAxis = this.focusYAxis; + const focusYScale = this.focusYScale; + + const svgHeight = getSvgHeight(); + + // Clear any existing elements from the visualization, + // then build the svg elements for the bubble chart. + const chartElement = d3.select(this.rootNode); + chartElement.selectAll('*').remove(); + + if (typeof selectedJob !== 'undefined') { + this.fieldFormat = mlFieldFormatService.getFieldFormat(selectedJob.job_id, detectorIndex); + } else { + return; + } - const fieldFormat = this.fieldFormat; + if (contextChartData === undefined) { + return; + } - const svg = chartElement - .append('svg') - .attr('width', svgWidth) - .attr('height', svgHeight); + const fieldFormat = this.fieldFormat; - let contextDataMin; - let contextDataMax; - if ( - modelPlotEnabled === true || - (contextForecastData !== undefined && contextForecastData.length > 0) - ) { - const combinedData = - contextForecastData === undefined - ? contextChartData - : contextChartData.concat(contextForecastData); + const svg = chartElement + .append('svg') + .attr('width', svgWidth) + .attr('height', svgHeight); - contextDataMin = d3.min(combinedData, d => Math.min(d.value, d.lower)); - contextDataMax = d3.max(combinedData, d => Math.max(d.value, d.upper)); - } else { - contextDataMin = d3.min(contextChartData, d => d.value); - contextDataMax = d3.max(contextChartData, d => d.value); - } + let contextDataMin; + let contextDataMax; + if ( + modelPlotEnabled === true || + (contextForecastData !== undefined && contextForecastData.length > 0) + ) { + const combinedData = + contextForecastData === undefined + ? contextChartData + : contextChartData.concat(contextForecastData); + + contextDataMin = d3.min(combinedData, d => Math.min(d.value, d.lower)); + contextDataMax = d3.max(combinedData, d => Math.max(d.value, d.upper)); + } else { + contextDataMin = d3.min(contextChartData, d => d.value); + contextDataMax = d3.max(contextChartData, d => d.value); + } - // Set the size of the left margin according to the width of the largest y axis tick label. - // The min / max of the aggregated context chart data may be less than the min / max of the - // data which is displayed in the focus chart which is likely to be plotted at a lower - // aggregation interval. Therefore ceil the min / max with the higher absolute value to allow - // for extra space for chart labels which may have higher values than the context data - // e.g. aggregated max may be 9500, whereas focus plot max may be 11234. - const ceiledMax = - contextDataMax > 0 - ? Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMax)))) - : contextDataMax; - - const flooredMin = - contextDataMin >= 0 - ? contextDataMin - : -1 * Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMin)))); - - // Temporarily set the domain of the focus y axis to the min / max of the full context chart - // data range so that we can measure the maximum tick label width on temporary text elements. - focusYScale.domain([flooredMin, ceiledMax]); - - let maxYAxisLabelWidth = 0; - const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); - tempLabelText - .selectAll('text.temp.axis') - .data(focusYScale.ticks()) - .enter() - .append('text') - .text(d => { - if (fieldFormat !== undefined) { - return fieldFormat.convert(d, 'text'); - } else { - return focusYScale.tickFormat()(d); - } - }) - .each(function() { - maxYAxisLabelWidth = Math.max( - this.getBBox().width + focusYAxis.tickPadding(), - maxYAxisLabelWidth - ); - }) - .remove(); - d3.select('.temp-axis-label').remove(); - - margin.left = Math.max(maxYAxisLabelWidth, 40); - this.vizWidth = Math.max(svgWidth - margin.left - margin.right, 0); - focusXScale.range([0, this.vizWidth]); - focusYAxis.innerTickSize(-this.vizWidth); - - const focus = svg - .append('g') - .attr('class', 'focus-chart') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - const context = svg - .append('g') - .attr('class', 'context-chart') - .attr( - 'transform', - 'translate(' + margin.left + ',' + (focusHeight + margin.top + chartSpacing) + ')' + // Set the size of the left margin according to the width of the largest y axis tick label. + // The min / max of the aggregated context chart data may be less than the min / max of the + // data which is displayed in the focus chart which is likely to be plotted at a lower + // aggregation interval. Therefore ceil the min / max with the higher absolute value to allow + // for extra space for chart labels which may have higher values than the context data + // e.g. aggregated max may be 9500, whereas focus plot max may be 11234. + const ceiledMax = + contextDataMax > 0 + ? Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMax)))) + : contextDataMax; + + const flooredMin = + contextDataMin >= 0 + ? contextDataMin + : -1 * Math.pow(10, Math.ceil(Math.log10(Math.abs(contextDataMin)))); + + // Temporarily set the domain of the focus y axis to the min / max of the full context chart + // data range so that we can measure the maximum tick label width on temporary text elements. + focusYScale.domain([flooredMin, ceiledMax]); + + let maxYAxisLabelWidth = 0; + const tempLabelText = svg.append('g').attr('class', 'temp-axis-label tick'); + tempLabelText + .selectAll('text.temp.axis') + .data(focusYScale.ticks()) + .enter() + .append('text') + .text(d => { + if (fieldFormat !== undefined) { + return fieldFormat.convert(d, 'text'); + } else { + return focusYScale.tickFormat()(d); + } + }) + .each(function() { + maxYAxisLabelWidth = Math.max( + this.getBBox().width + focusYAxis.tickPadding(), + maxYAxisLabelWidth ); + }) + .remove(); + d3.select('.temp-axis-label').remove(); + + margin.left = Math.max(maxYAxisLabelWidth, 40); + this.vizWidth = Math.max(svgWidth - margin.left - margin.right, 0); + focusXScale.range([0, this.vizWidth]); + focusYAxis.innerTickSize(-this.vizWidth); + + const focus = svg + .append('g') + .attr('class', 'focus-chart') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + const context = svg + .append('g') + .attr('class', 'context-chart') + .attr( + 'transform', + 'translate(' + margin.left + ',' + (focusHeight + margin.top + chartSpacing) + ')' + ); - // Mask to hide annotations overflow - if (mlAnnotationsEnabled) { - const annotationsMask = svg - .append('defs') - .append('mask') - .attr('id', ANNOTATION_MASK_ID); - - annotationsMask - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', this.vizWidth) - .attr('height', focusHeight) - .style('fill', 'white'); - } + // Mask to hide annotations overflow + const annotationsMask = svg + .append('defs') + .append('mask') + .attr('id', ANNOTATION_MASK_ID); + + annotationsMask + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', this.vizWidth) + .attr('height', focusHeight) + .style('fill', 'white'); + + // Draw each of the component elements. + createFocusChart(focus, this.vizWidth, focusHeight); + drawContextElements(context, this.vizWidth, contextChartHeight, swimlaneHeight); + } - // Draw each of the component elements. - createFocusChart(focus, this.vizWidth, focusHeight); - drawContextElements(context, this.vizWidth, contextChartHeight, swimlaneHeight); + contextChartInitialized = false; + drawContextChartSelection() { + const { + contextChartData, + contextChartSelected, + contextForecastData, + zoomFrom, + zoomTo, + } = this.props; + + if (contextChartData === undefined) { + return; } - contextChartInitialized = false; - drawContextChartSelection() { - const { - contextChartData, - contextChartSelected, - contextForecastData, - zoomFrom, - zoomTo, - } = this.props; - - if (contextChartData === undefined) { - return; - } - - // Make appropriate selection in the context chart to trigger loading of the focus chart. - let focusLoadFrom; - let focusLoadTo; - const contextXMin = this.contextXScale.domain()[0].getTime(); - const contextXMax = this.contextXScale.domain()[1].getTime(); + // Make appropriate selection in the context chart to trigger loading of the focus chart. + let focusLoadFrom; + let focusLoadTo; + const contextXMin = this.contextXScale.domain()[0].getTime(); + const contextXMax = this.contextXScale.domain()[1].getTime(); - let combinedData = contextChartData; - if (contextForecastData !== undefined) { - combinedData = combinedData.concat(contextForecastData); - } - - if (zoomFrom) { - focusLoadFrom = zoomFrom.getTime(); - } else { - focusLoadFrom = _.reduce( - combinedData, - (memo, point) => Math.min(memo, point.date.getTime()), - new Date(2099, 12, 31).getTime() - ); - } - focusLoadFrom = Math.max(focusLoadFrom, contextXMin); + let combinedData = contextChartData; + if (contextForecastData !== undefined) { + combinedData = combinedData.concat(contextForecastData); + } - if (zoomTo) { - focusLoadTo = zoomTo.getTime(); - } else { - focusLoadTo = _.reduce( - combinedData, - (memo, point) => Math.max(memo, point.date.getTime()), - 0 - ); - } - focusLoadTo = Math.min(focusLoadTo, contextXMax); + if (zoomFrom) { + focusLoadFrom = zoomFrom.getTime(); + } else { + focusLoadFrom = _.reduce( + combinedData, + (memo, point) => Math.min(memo, point.date.getTime()), + new Date(2099, 12, 31).getTime() + ); + } + focusLoadFrom = Math.max(focusLoadFrom, contextXMin); + + if (zoomTo) { + focusLoadTo = zoomTo.getTime(); + } else { + focusLoadTo = _.reduce( + combinedData, + (memo, point) => Math.max(memo, point.date.getTime()), + 0 + ); + } + focusLoadTo = Math.min(focusLoadTo, contextXMax); - const brushVisibility = focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax; - this.setBrushVisibility(brushVisibility); + const brushVisibility = focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax; + this.setBrushVisibility(brushVisibility); - if (focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax) { - this.setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); - const newSelectedBounds = { - min: moment(new Date(focusLoadFrom)), - max: moment(focusLoadFrom), - }; + if (focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax) { + this.setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); + const newSelectedBounds = { + min: moment(new Date(focusLoadFrom)), + max: moment(focusLoadFrom), + }; + this.selectedBounds = newSelectedBounds; + } else { + const contextXScaleDomain = this.contextXScale.domain(); + const newSelectedBounds = { + min: moment(new Date(contextXScaleDomain[0])), + max: moment(contextXScaleDomain[1]), + }; + if (!_.isEqual(newSelectedBounds, this.selectedBounds)) { this.selectedBounds = newSelectedBounds; - } else { - const contextXScaleDomain = this.contextXScale.domain(); - const newSelectedBounds = { - min: moment(new Date(contextXScaleDomain[0])), - max: moment(contextXScaleDomain[1]), - }; - if (!_.isEqual(newSelectedBounds, this.selectedBounds)) { - this.selectedBounds = newSelectedBounds; - if (this.contextChartInitialized === false) { - this.contextChartInitialized = true; - contextChartSelected({ from: contextXScaleDomain[0], to: contextXScaleDomain[1] }); - } + if (this.contextChartInitialized === false) { + this.contextChartInitialized = true; + contextChartSelected({ from: contextXScaleDomain[0], to: contextXScaleDomain[1] }); } } } + } - createFocusChart(fcsGroup, fcsWidth, fcsHeight) { - // Split out creation of the focus chart from the rendering, - // as we want to re-render the paths and points when the zoom area changes. - - const { contextForecastData } = this.props; - - // Add a group at the top to display info on the chart aggregation interval - // and links to set the brush span to 1h, 1d, 1w etc. - const zoomGroup = fcsGroup.append('g').attr('class', 'focus-zoom'); - zoomGroup - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', fcsWidth) - .attr('height', focusZoomPanelHeight) - .attr('class', 'chart-border'); - this.createZoomInfoElements(zoomGroup, fcsWidth); - - // Create the elements for annotations - if (mlAnnotationsEnabled) { - const annotateBrush = this.annotateBrush.bind(this); - - let brushX = 0; - let brushWidth = 0; - - if (this.props.annotation !== null) { - // If the annotation brush is showing, set it to the same position - brushX = this.focusXScale(this.props.annotation.timestamp); - brushWidth = getAnnotationWidth(this.props.annotation, this.focusXScale); - } + createFocusChart(fcsGroup, fcsWidth, fcsHeight) { + // Split out creation of the focus chart from the rendering, + // as we want to re-render the paths and points when the zoom area changes. + + const { contextForecastData } = this.props; + + // Add a group at the top to display info on the chart aggregation interval + // and links to set the brush span to 1h, 1d, 1w etc. + const zoomGroup = fcsGroup.append('g').attr('class', 'focus-zoom'); + zoomGroup + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', fcsWidth) + .attr('height', focusZoomPanelHeight) + .attr('class', 'chart-border'); + this.createZoomInfoElements(zoomGroup, fcsWidth); + + // Create the elements for annotations + const annotateBrush = this.annotateBrush.bind(this); + + let brushX = 0; + let brushWidth = 0; + + if (this.props.annotation !== null) { + // If the annotation brush is showing, set it to the same position + brushX = this.focusXScale(this.props.annotation.timestamp); + brushWidth = getAnnotationWidth(this.props.annotation, this.focusXScale); + } - fcsGroup - .append('g') - .attr('class', 'mlAnnotationBrush') - .call(annotateBrush) - .selectAll('rect') - .attr('x', brushX) - .attr('y', focusZoomPanelHeight) - .attr('width', brushWidth) - .attr('height', focusChartHeight); - - fcsGroup.append('g').classed('mlAnnotations', true); - } + fcsGroup + .append('g') + .attr('class', 'mlAnnotationBrush') + .call(annotateBrush) + .selectAll('rect') + .attr('x', brushX) + .attr('y', focusZoomPanelHeight) + .attr('width', brushWidth) + .attr('height', focusChartHeight); + + fcsGroup.append('g').classed('mlAnnotations', true); + + // Add border round plot area. + fcsGroup + .append('rect') + .attr('x', 0) + .attr('y', focusZoomPanelHeight) + .attr('width', fcsWidth) + .attr('height', focusChartHeight) + .attr('class', 'chart-border'); + + // Add background for x axis. + const xAxisBg = fcsGroup.append('g').attr('class', 'x-axis-background'); + xAxisBg + .append('rect') + .attr('x', 0) + .attr('y', fcsHeight) + .attr('width', fcsWidth) + .attr('height', chartSpacing); + xAxisBg + .append('line') + .attr('x1', 0) + .attr('y1', fcsHeight) + .attr('x2', 0) + .attr('y2', fcsHeight + chartSpacing); + xAxisBg + .append('line') + .attr('x1', fcsWidth) + .attr('y1', fcsHeight) + .attr('x2', fcsWidth) + .attr('y2', fcsHeight + chartSpacing); + xAxisBg + .append('line') + .attr('x1', 0) + .attr('y1', fcsHeight + chartSpacing) + .attr('x2', fcsWidth) + .attr('y2', fcsHeight + chartSpacing); + + const axes = fcsGroup.append('g'); + axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + fcsHeight + ')'); + axes.append('g').attr('class', 'y axis'); + + // Create the elements for the metric value line and model bounds area. + fcsGroup.append('path').attr('class', 'area bounds'); + fcsGroup.append('path').attr('class', 'values-line'); + fcsGroup.append('g').attr('class', 'focus-chart-markers'); + + // Create the path elements for the forecast value line and bounds area. + if (contextForecastData) { + fcsGroup.append('path').attr('class', 'area forecast'); + fcsGroup.append('path').attr('class', 'values-line forecast'); + fcsGroup.append('g').attr('class', 'focus-chart-markers forecast'); + } - // Add border round plot area. - fcsGroup - .append('rect') - .attr('x', 0) - .attr('y', focusZoomPanelHeight) - .attr('width', fcsWidth) - .attr('height', focusChartHeight) - .attr('class', 'chart-border'); - - // Add background for x axis. - const xAxisBg = fcsGroup.append('g').attr('class', 'x-axis-background'); - xAxisBg - .append('rect') - .attr('x', 0) - .attr('y', fcsHeight) - .attr('width', fcsWidth) - .attr('height', chartSpacing); - xAxisBg - .append('line') - .attr('x1', 0) - .attr('y1', fcsHeight) - .attr('x2', 0) - .attr('y2', fcsHeight + chartSpacing); - xAxisBg - .append('line') - .attr('x1', fcsWidth) - .attr('y1', fcsHeight) - .attr('x2', fcsWidth) - .attr('y2', fcsHeight + chartSpacing); - xAxisBg - .append('line') - .attr('x1', 0) - .attr('y1', fcsHeight + chartSpacing) - .attr('x2', fcsWidth) - .attr('y2', fcsHeight + chartSpacing); - - const axes = fcsGroup.append('g'); - axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + fcsHeight + ')'); - axes.append('g').attr('class', 'y axis'); - - // Create the elements for the metric value line and model bounds area. - fcsGroup.append('path').attr('class', 'area bounds'); - fcsGroup.append('path').attr('class', 'values-line'); - fcsGroup.append('g').attr('class', 'focus-chart-markers'); - - // Create the path elements for the forecast value line and bounds area. - if (contextForecastData) { - fcsGroup.append('path').attr('class', 'area forecast'); - fcsGroup.append('path').attr('class', 'values-line forecast'); - fcsGroup.append('g').attr('class', 'focus-chart-markers forecast'); - } + fcsGroup + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', fcsWidth) + .attr('height', fcsHeight + 24) + .attr('class', 'chart-border chart-border-highlight'); + } - fcsGroup - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', fcsWidth) - .attr('height', fcsHeight + 24) - .attr('class', 'chart-border chart-border-highlight'); + renderFocusChart() { + const { + focusAggregationInterval, + focusAnnotationData, + focusChartData, + focusForecastData, + modelPlotEnabled, + selectedJob, + showAnnotations, + showForecast, + showModelBounds, + + zoomFromFocusLoaded, + zoomToFocusLoaded, + } = this.props; + + if (focusChartData === undefined) { + return; } - renderFocusChart() { - const { - focusAggregationInterval, - focusAnnotationData, - focusChartData, - focusForecastData, - modelPlotEnabled, - selectedJob, - showAnnotations, - showForecast, - showModelBounds, - intl, - zoomFromFocusLoaded, - zoomToFocusLoaded, - } = this.props; - - if (focusChartData === undefined) { - return; - } + const data = focusChartData; - const data = focusChartData; + const contextYScale = this.contextYScale; + const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); - const contextYScale = this.contextYScale; - const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + const focusChart = d3.select('.focus-chart'); - const focusChart = d3.select('.focus-chart'); + // Update the plot interval labels. + const focusAggInt = focusAggregationInterval.expression; + const bucketSpan = selectedJob.analysis_config.bucket_span; + const chartElement = d3.select(this.rootNode); + chartElement.select('.zoom-aggregation-interval').text( + i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomAggregationIntervalLabel', { + defaultMessage: '(aggregation interval: {focusAggInt}, bucket span: {bucketSpan})', + values: { focusAggInt, bucketSpan }, + }) + ); - // Update the plot interval labels. - const focusAggInt = focusAggregationInterval.expression; - const bucketSpan = selectedJob.analysis_config.bucket_span; - const chartElement = d3.select(this.rootNode); - chartElement.select('.zoom-aggregation-interval').text( - intl.formatMessage( - { - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomAggregationIntervalLabel', - defaultMessage: '(aggregation interval: {focusAggInt}, bucket span: {bucketSpan})', - }, - { focusAggInt, bucketSpan } - ) - ); + // Render the axes. - // Render the axes. + // Calculate the x axis domain. + // Elasticsearch aggregation returns points at start of bucket, + // so set the x-axis min to the start of the first aggregation interval, + // and the x-axis max to the end of the last aggregation interval. + if (zoomFromFocusLoaded === undefined || zoomToFocusLoaded === undefined) { + return; + } + const bounds = { + min: moment(zoomFromFocusLoaded.getTime()), + max: moment(zoomToFocusLoaded.getTime()), + }; - // Calculate the x axis domain. - // Elasticsearch aggregation returns points at start of bucket, - // so set the x-axis min to the start of the first aggregation interval, - // and the x-axis max to the end of the last aggregation interval. - if (zoomFromFocusLoaded === undefined || zoomToFocusLoaded === undefined) { - return; + const aggMs = focusAggregationInterval.asMilliseconds(); + const earliest = moment(Math.floor(bounds.min.valueOf() / aggMs) * aggMs); + const latest = moment(Math.ceil(bounds.max.valueOf() / aggMs) * aggMs); + this.focusXScale.domain([earliest.toDate(), latest.toDate()]); + + // Calculate the y-axis domain. + if ( + focusChartData.length > 0 || + (focusForecastData !== undefined && focusForecastData.length > 0) + ) { + if (this.fieldFormat !== undefined) { + this.focusYAxis.tickFormat(d => this.fieldFormat.convert(d, 'text')); + } else { + // Use default tick formatter. + this.focusYAxis.tickFormat(null); } - const bounds = { - min: moment(zoomFromFocusLoaded.getTime()), - max: moment(zoomToFocusLoaded.getTime()), - }; - const aggMs = focusAggregationInterval.asMilliseconds(); - const earliest = moment(Math.floor(bounds.min.valueOf() / aggMs) * aggMs); - const latest = moment(Math.ceil(bounds.max.valueOf() / aggMs) * aggMs); - this.focusXScale.domain([earliest.toDate(), latest.toDate()]); + // Calculate the min/max of the metric data and the forecast data. + let yMin = 0; + let yMax = 0; - // Calculate the y-axis domain. - if ( - focusChartData.length > 0 || - (focusForecastData !== undefined && focusForecastData.length > 0) - ) { - if (this.fieldFormat !== undefined) { - this.focusYAxis.tickFormat(d => this.fieldFormat.convert(d, 'text')); - } else { - // Use default tick formatter. - this.focusYAxis.tickFormat(null); - } - - // Calculate the min/max of the metric data and the forecast data. - let yMin = 0; - let yMax = 0; + let combinedData = data; + if (focusForecastData !== undefined && focusForecastData.length > 0) { + combinedData = data.concat(focusForecastData); + } - let combinedData = data; - if (focusForecastData !== undefined && focusForecastData.length > 0) { - combinedData = data.concat(focusForecastData); + yMin = d3.min(combinedData, d => { + let metricValue = d.value; + if (metricValue === null && d.anomalyScore !== undefined && d.actual !== undefined) { + // If an anomaly coincides with a gap in the data, use the anomaly actual value. + metricValue = Array.isArray(d.actual) ? d.actual[0] : d.actual; } - - yMin = d3.min(combinedData, d => { - let metricValue = d.value; - if (metricValue === null && d.anomalyScore !== undefined && d.actual !== undefined) { - // If an anomaly coincides with a gap in the data, use the anomaly actual value. - metricValue = Array.isArray(d.actual) ? d.actual[0] : d.actual; - } - if (d.lower !== undefined) { - if (metricValue !== null && metricValue !== undefined) { - return Math.min(metricValue, d.lower); - } else { - // Set according to the minimum of the lower of the model plot results. - return d.lower; - } - } - return metricValue; - }); - yMax = d3.max(combinedData, d => { - let metricValue = d.value; - if (metricValue === null && d.anomalyScore !== undefined && d.actual !== undefined) { - // If an anomaly coincides with a gap in the data, use the anomaly actual value. - metricValue = Array.isArray(d.actual) ? d.actual[0] : d.actual; - } - return d.upper !== undefined ? Math.max(metricValue, d.upper) : metricValue; - }); - - if (yMax === yMin) { - if ( - this.contextYScale.domain()[0] !== contextYScale.domain()[1] && - yMin >= contextYScale.domain()[0] && - yMax <= contextYScale.domain()[1] - ) { - // Set the focus chart limits to be the same as the context chart. - yMin = contextYScale.domain()[0]; - yMax = contextYScale.domain()[1]; + if (d.lower !== undefined) { + if (metricValue !== null && metricValue !== undefined) { + return Math.min(metricValue, d.lower); } else { - yMin -= yMin * 0.05; - yMax += yMax * 0.05; + // Set according to the minimum of the lower of the model plot results. + return d.lower; } } + return metricValue; + }); + yMax = d3.max(combinedData, d => { + let metricValue = d.value; + if (metricValue === null && d.anomalyScore !== undefined && d.actual !== undefined) { + // If an anomaly coincides with a gap in the data, use the anomaly actual value. + metricValue = Array.isArray(d.actual) ? d.actual[0] : d.actual; + } + return d.upper !== undefined ? Math.max(metricValue, d.upper) : metricValue; + }); - // if annotations are present, we extend yMax to avoid overlap - // between annotation labels, chart lines and anomalies. - if (mlAnnotationsEnabled && focusAnnotationData && focusAnnotationData.length > 0) { - const levels = getAnnotationLevels(focusAnnotationData); - const maxLevel = d3.max(Object.keys(levels).map(key => levels[key])); - // TODO needs revisiting to be a more robust normalization - yMax = yMax * (1 + (maxLevel + 1) / 5); + if (yMax === yMin) { + if ( + this.contextYScale.domain()[0] !== contextYScale.domain()[1] && + yMin >= contextYScale.domain()[0] && + yMax <= contextYScale.domain()[1] + ) { + // Set the focus chart limits to be the same as the context chart. + yMin = contextYScale.domain()[0]; + yMax = contextYScale.domain()[1]; + } else { + yMin -= yMin * 0.05; + yMax += yMax * 0.05; } - this.focusYScale.domain([yMin, yMax]); - } else { - // Display 10 unlabelled ticks. - this.focusYScale.domain([0, 10]); - this.focusYAxis.tickFormat(''); } - // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); - timeBuckets.setInterval('auto'); - timeBuckets.setBounds(bounds); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - focusChart.select('.x.axis').call( - this.focusXAxis - .ticks(numTicksForDateFormat(this.vizWidth), xAxisTickFormat) - .tickFormat(d => { - return moment(d).format(xAxisTickFormat); - }) - ); - focusChart.select('.y.axis').call(this.focusYAxis); - - filterAxisLabels(focusChart.select('.x.axis'), this.vizWidth); - - // Render the bounds area and values line. - if (modelPlotEnabled === true) { - focusChart - .select('.area.bounds') - .attr('d', this.focusBoundedArea(data)) - .classed('hidden', !showModelBounds); + // if annotations are present, we extend yMax to avoid overlap + // between annotation labels, chart lines and anomalies. + if (focusAnnotationData && focusAnnotationData.length > 0) { + const levels = getAnnotationLevels(focusAnnotationData); + const maxLevel = d3.max(Object.keys(levels).map(key => levels[key])); + // TODO needs revisiting to be a more robust normalization + yMax = yMax * (1 + (maxLevel + 1) / 5); } + this.focusYScale.domain([yMin, yMax]); + } else { + // Display 10 unlabelled ticks. + this.focusYScale.domain([0, 10]); + this.focusYAxis.tickFormat(''); + } - if (mlAnnotationsEnabled) { - renderAnnotations( - focusChart, - focusAnnotationData, - focusZoomPanelHeight, - focusChartHeight, - this.focusXScale, - showAnnotations, - showFocusChartTooltip - ); + // Get the scaled date format to use for x axis tick labels. + const timeBuckets = new TimeBuckets(); + timeBuckets.setInterval('auto'); + timeBuckets.setBounds(bounds); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + focusChart.select('.x.axis').call( + this.focusXAxis.ticks(numTicksForDateFormat(this.vizWidth), xAxisTickFormat).tickFormat(d => { + return moment(d).format(xAxisTickFormat); + }) + ); + focusChart.select('.y.axis').call(this.focusYAxis); + + filterAxisLabels(focusChart.select('.x.axis'), this.vizWidth); + + // Render the bounds area and values line. + if (modelPlotEnabled === true) { + focusChart + .select('.area.bounds') + .attr('d', this.focusBoundedArea(data)) + .classed('hidden', !showModelBounds); + } - // disable brushing (creation of annotations) when annotations aren't shown - focusChart.select('.mlAnnotationBrush').style('display', showAnnotations ? null : 'none'); - } + renderAnnotations( + focusChart, + focusAnnotationData, + focusZoomPanelHeight, + focusChartHeight, + this.focusXScale, + showAnnotations, + showFocusChartTooltip + ); + + // disable brushing (creation of annotations) when annotations aren't shown + focusChart.select('.mlAnnotationBrush').style('display', showAnnotations ? null : 'none'); + + focusChart.select('.values-line').attr('d', this.focusValuesLine(data)); + drawLineChartDots(data, focusChart, this.focusValuesLine); + + // Render circle markers for the points. + // These are used for displaying tooltips on mouseover. + // Don't render dots where value=null (data gaps, with no anomalies) + // or for multi-bucket anomalies. + const dots = d3 + .select('.focus-chart-markers') + .selectAll('.metric-value') + .data( + data.filter( + d => + (d.value !== null || typeof d.anomalyScore === 'number') && + !showMultiBucketAnomalyMarker(d) + ) + ); - focusChart.select('.values-line').attr('d', this.focusValuesLine(data)); - drawLineChartDots(data, focusChart, this.focusValuesLine); + // Remove dots that are no longer needed i.e. if number of chart points has decreased. + dots.exit().remove(); + // Create any new dots that are needed i.e. if number of chart points has increased. + dots + .enter() + .append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + .on('mouseover', function(d) { + showFocusChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all dots to new positions. + dots + .attr('cx', d => { + return this.focusXScale(d.date); + }) + .attr('cy', d => { + return this.focusYScale(d.value); + }) + .attr('class', d => { + let markerClass = 'metric-value'; + if (_.has(d, 'anomalyScore')) { + markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`; + } + return markerClass; + }); - // Render circle markers for the points. - // These are used for displaying tooltips on mouseover. - // Don't render dots where value=null (data gaps, with no anomalies) - // or for multi-bucket anomalies. - const dots = d3 - .select('.focus-chart-markers') + // Render cross symbols for any multi-bucket anomalies. + const multiBucketMarkers = d3 + .select('.focus-chart-markers') + .selectAll('.multi-bucket') + .data(data.filter(d => d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true)); + + // Remove multi-bucket markers that are no longer needed. + multiBucketMarkers.exit().remove(); + + // Add any new markers that are needed i.e. if number of multi-bucket points has increased. + multiBucketMarkers + .enter() + .append('path') + .attr( + 'd', + d3.svg + .symbol() + .size(MULTI_BUCKET_SYMBOL_SIZE) + .type('cross') + ) + .on('mouseover', function(d) { + showFocusChartTooltip(d, this); + }) + .on('mouseout', () => mlChartTooltipService.hide()); + + // Update all markers to new positions. + multiBucketMarkers + .attr( + 'transform', + d => `translate(${this.focusXScale(d.date)}, ${this.focusYScale(d.value)})` + ) + .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}`); + + // Add rectangular markers for any scheduled events. + const scheduledEventMarkers = d3 + .select('.focus-chart-markers') + .selectAll('.scheduled-event-marker') + .data(data.filter(d => d.scheduledEvents !== undefined)); + + // Remove markers that are no longer needed i.e. if number of chart points has decreased. + scheduledEventMarkers.exit().remove(); + + // Create any new markers that are needed i.e. if number of chart points has increased. + scheduledEventMarkers + .enter() + .append('rect') + .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) + .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) + .attr('class', 'scheduled-event-marker') + .attr('rx', 1) + .attr('ry', 1); + + // Update all markers to new positions. + scheduledEventMarkers + .attr('x', d => this.focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) + .attr('y', d => this.focusYScale(d.value) - 3); + + // Plot any forecast data in scope. + if (focusForecastData !== undefined) { + focusChart + .select('.area.forecast') + .attr('d', this.focusBoundedArea(focusForecastData)) + .classed('hidden', !showForecast); + focusChart + .select('.values-line.forecast') + .attr('d', this.focusValuesLine(focusForecastData)) + .classed('hidden', !showForecast); + + const forecastDots = d3 + .select('.focus-chart-markers.forecast') .selectAll('.metric-value') - .data( - data.filter( - d => - (d.value !== null || typeof d.anomalyScore === 'number') && - !showMultiBucketAnomalyMarker(d) - ) - ); + .data(focusForecastData); - // Remove dots that are no longer needed i.e. if number of chart points has decreased. - dots.exit().remove(); - // Create any new dots that are needed i.e. if number of chart points has increased. - dots + // Remove dots that are no longer needed i.e. if number of forecast points has decreased. + forecastDots.exit().remove(); + // Create any new dots that are needed i.e. if number of forecast points has increased. + forecastDots .enter() .append('circle') .attr('r', LINE_CHART_ANOMALY_RADIUS) @@ -772,755 +856,603 @@ const TimeseriesChartIntl = injectI18n( .on('mouseout', () => mlChartTooltipService.hide()); // Update all dots to new positions. - dots + forecastDots .attr('cx', d => { return this.focusXScale(d.date); }) .attr('cy', d => { return this.focusYScale(d.value); }) - .attr('class', d => { - let markerClass = 'metric-value'; - if (_.has(d, 'anomalyScore')) { - markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`; - } - return markerClass; - }); - - // Render cross symbols for any multi-bucket anomalies. - const multiBucketMarkers = d3 - .select('.focus-chart-markers') - .selectAll('.multi-bucket') - .data( - data.filter(d => d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true) - ); - - // Remove multi-bucket markers that are no longer needed. - multiBucketMarkers.exit().remove(); + .attr('class', 'metric-value') + .classed('hidden', !showForecast); + } + } - // Add any new markers that are needed i.e. if number of multi-bucket points has increased. - multiBucketMarkers - .enter() - .append('path') - .attr( - 'd', - d3.svg - .symbol() - .size(MULTI_BUCKET_SYMBOL_SIZE) - .type('cross') - ) - .on('mouseover', function(d) { - showFocusChartTooltip(d, this); + createZoomInfoElements(zoomGroup, fcsWidth) { + const { autoZoomDuration, bounds, modelPlotEnabled } = this.props; + + const setZoomInterval = this.setZoomInterval.bind(this); + + // Create zoom duration links applicable for the current time span. + // Don't add links for any durations which would give a brush extent less than 10px. + const boundsSecs = bounds.max.unix() - bounds.min.unix(); + const minSecs = (10 / this.vizWidth) * boundsSecs; + + let xPos = 10; + const zoomLabel = zoomGroup + .append('text') + .attr('x', xPos) + .attr('y', 17) + .attr('class', 'zoom-info-text') + .text( + i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomLabel', { + defaultMessage: 'Zoom:', }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Update all markers to new positions. - multiBucketMarkers - .attr( - 'transform', - d => `translate(${this.focusXScale(d.date)}, ${this.focusYScale(d.value)})` - ) - .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}`); - - // Add rectangular markers for any scheduled events. - const scheduledEventMarkers = d3 - .select('.focus-chart-markers') - .selectAll('.scheduled-event-marker') - .data(data.filter(d => d.scheduledEvents !== undefined)); - - // Remove markers that are no longer needed i.e. if number of chart points has decreased. - scheduledEventMarkers.exit().remove(); + ); - // Create any new markers that are needed i.e. if number of chart points has increased. - scheduledEventMarkers - .enter() - .append('rect') - .attr('width', LINE_CHART_ANOMALY_RADIUS * 2) - .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT) - .attr('class', 'scheduled-event-marker') - .attr('rx', 1) - .attr('ry', 1); - - // Update all markers to new positions. - scheduledEventMarkers - .attr('x', d => this.focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS) - .attr('y', d => this.focusYScale(d.value) - 3); - - // Plot any forecast data in scope. - if (focusForecastData !== undefined) { - focusChart - .select('.area.forecast') - .attr('d', this.focusBoundedArea(focusForecastData)) - .classed('hidden', !showForecast); - focusChart - .select('.values-line.forecast') - .attr('d', this.focusValuesLine(focusForecastData)) - .classed('hidden', !showForecast); - - const forecastDots = d3 - .select('.focus-chart-markers.forecast') - .selectAll('.metric-value') - .data(focusForecastData); - - // Remove dots that are no longer needed i.e. if number of forecast points has decreased. - forecastDots.exit().remove(); - // Create any new dots that are needed i.e. if number of forecast points has increased. - forecastDots - .enter() - .append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - .on('mouseover', function(d) { - showFocusChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - // Update all dots to new positions. - forecastDots - .attr('cx', d => { - return this.focusXScale(d.date); - }) - .attr('cy', d => { - return this.focusYScale(d.value); - }) - .attr('class', 'metric-value') - .classed('hidden', !showForecast); + const zoomOptions = [{ durationMs: autoZoomDuration, label: 'auto' }]; + _.each(ZOOM_INTERVAL_OPTIONS, option => { + if (option.duration.asSeconds() > minSecs && option.duration.asSeconds() < boundsSecs) { + zoomOptions.push({ durationMs: option.duration.asMilliseconds(), label: option.label }); } - } - - createZoomInfoElements(zoomGroup, fcsWidth) { - const { autoZoomDuration, bounds, modelPlotEnabled, intl } = this.props; - - const setZoomInterval = this.setZoomInterval.bind(this); - - // Create zoom duration links applicable for the current time span. - // Don't add links for any durations which would give a brush extent less than 10px. - const boundsSecs = bounds.max.unix() - bounds.min.unix(); - const minSecs = (10 / this.vizWidth) * boundsSecs; - - let xPos = 10; - const zoomLabel = zoomGroup + }); + xPos += zoomLabel.node().getBBox().width + 4; + + _.each(zoomOptions, option => { + const text = zoomGroup + .append('a') + .attr('data-ms', option.durationMs) + .attr('href', '') .append('text') .attr('x', xPos) .attr('y', 17) .attr('class', 'zoom-info-text') - .text( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomLabel', - defaultMessage: 'Zoom:', - }) - ); - - const zoomOptions = [{ durationMs: autoZoomDuration, label: 'auto' }]; - _.each(ZOOM_INTERVAL_OPTIONS, option => { - if (option.duration.asSeconds() > minSecs && option.duration.asSeconds() < boundsSecs) { - zoomOptions.push({ durationMs: option.duration.asMilliseconds(), label: option.label }); - } - }); - xPos += zoomLabel.node().getBBox().width + 4; - - _.each(zoomOptions, option => { - const text = zoomGroup - .append('a') - .attr('data-ms', option.durationMs) - .attr('href', '') - .append('text') - .attr('x', xPos) - .attr('y', 17) - .attr('class', 'zoom-info-text') - .text(option.label); - - xPos += text.node().getBBox().width + 4; - }); + .text(option.label); + + xPos += text.node().getBBox().width + 4; + }); + + zoomGroup + .append('text') + .attr('x', xPos + 6) + .attr('y', 17) + .attr('class', 'zoom-info-text zoom-aggregation-interval') + .text( + i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomGroupAggregationIntervalLabel', + { + defaultMessage: '(aggregation interval: , bucket span: )', + } + ) + ); - zoomGroup + if (modelPlotEnabled === false) { + const modelPlotLabel = zoomGroup .append('text') - .attr('x', xPos + 6) + .attr('x', 300) .attr('y', 17) - .attr('class', 'zoom-info-text zoom-aggregation-interval') + .attr('class', 'zoom-info-text') .text( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomGroupAggregationIntervalLabel', - defaultMessage: '(aggregation interval: , bucket span: )', - }) - ); - - if (modelPlotEnabled === false) { - const modelPlotLabel = zoomGroup - .append('text') - .attr('x', 300) - .attr('y', 17) - .attr('class', 'zoom-info-text') - .text( - intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelBoundsNotAvailableLabel', + i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelBoundsNotAvailableLabel', + { defaultMessage: 'Model bounds are not available', - }) - ); - - modelPlotLabel.attr('x', fcsWidth - (modelPlotLabel.node().getBBox().width + 10)); - } + } + ) + ); - const chartElement = d3.select(this.rootNode); - chartElement.selectAll('.focus-zoom a').on('click', function() { - d3.event.preventDefault(); - setZoomInterval(d3.select(this).attr('data-ms')); - }); + modelPlotLabel.attr('x', fcsWidth - (modelPlotLabel.node().getBBox().width + 10)); } - drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { - const { bounds, contextChartData, contextForecastData, modelPlotEnabled } = this.props; - - const data = contextChartData; - - this.contextXScale = d3.time - .scale() - .range([0, cxtWidth]) - .domain(this.calculateContextXAxisDomain()); + const chartElement = d3.select(this.rootNode); + chartElement.selectAll('.focus-zoom a').on('click', function() { + d3.event.preventDefault(); + setZoomInterval(d3.select(this).attr('data-ms')); + }); + } - const combinedData = - contextForecastData === undefined ? data : data.concat(contextForecastData); - const valuesRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; + drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { + const { bounds, contextChartData, contextForecastData, modelPlotEnabled } = this.props; + + const data = contextChartData; + + this.contextXScale = d3.time + .scale() + .range([0, cxtWidth]) + .domain(this.calculateContextXAxisDomain()); + + const combinedData = + contextForecastData === undefined ? data : data.concat(contextForecastData); + const valuesRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; + _.each(combinedData, item => { + valuesRange.min = Math.min(item.value, valuesRange.min); + valuesRange.max = Math.max(item.value, valuesRange.max); + }); + let dataMin = valuesRange.min; + let dataMax = valuesRange.max; + const chartLimits = { min: dataMin, max: dataMax }; + + if ( + modelPlotEnabled === true || + (contextForecastData !== undefined && contextForecastData.length > 0) + ) { + const boundsRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; _.each(combinedData, item => { - valuesRange.min = Math.min(item.value, valuesRange.min); - valuesRange.max = Math.max(item.value, valuesRange.max); + boundsRange.min = Math.min(item.lower, boundsRange.min); + boundsRange.max = Math.max(item.upper, boundsRange.max); }); - let dataMin = valuesRange.min; - let dataMax = valuesRange.max; - const chartLimits = { min: dataMin, max: dataMax }; + dataMin = Math.min(dataMin, boundsRange.min); + dataMax = Math.max(dataMax, boundsRange.max); - if ( - modelPlotEnabled === true || - (contextForecastData !== undefined && contextForecastData.length > 0) - ) { - const boundsRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE }; - _.each(combinedData, item => { - boundsRange.min = Math.min(item.lower, boundsRange.min); - boundsRange.max = Math.max(item.upper, boundsRange.max); - }); - dataMin = Math.min(dataMin, boundsRange.min); - dataMax = Math.max(dataMax, boundsRange.max); - - // Set the y axis domain so that the range of actual values takes up at least 50% of the full range. - if (valuesRange.max - valuesRange.min < 0.5 * (dataMax - dataMin)) { - if (valuesRange.min > dataMin) { - chartLimits.min = valuesRange.min - 0.5 * (valuesRange.max - valuesRange.min); - } - - if (valuesRange.max < dataMax) { - chartLimits.max = valuesRange.max + 0.5 * (valuesRange.max - valuesRange.min); - } + // Set the y axis domain so that the range of actual values takes up at least 50% of the full range. + if (valuesRange.max - valuesRange.min < 0.5 * (dataMax - dataMin)) { + if (valuesRange.min > dataMin) { + chartLimits.min = valuesRange.min - 0.5 * (valuesRange.max - valuesRange.min); } - } - - this.contextYScale = d3.scale - .linear() - .range([cxtChartHeight, contextChartLineTopMargin]) - .domain([chartLimits.min, chartLimits.max]); - - const borders = cxtGroup.append('g').attr('class', 'axis'); - - // Add borders left and right. - borders - .append('line') - .attr('x1', 0) - .attr('y1', 0) - .attr('x2', 0) - .attr('y2', cxtChartHeight + swlHeight); - borders - .append('line') - .attr('x1', cxtWidth) - .attr('y1', 0) - .attr('x2', cxtWidth) - .attr('y2', cxtChartHeight + swlHeight); - - // Add x axis. - const timeBuckets = new TimeBuckets(); - timeBuckets.setInterval('auto'); - timeBuckets.setBounds(bounds); - const xAxisTickFormat = timeBuckets.getScaledDateFormat(); - const xAxis = d3.svg - .axis() - .scale(this.contextXScale) - .orient('top') - .innerTickSize(-cxtChartHeight) - .outerTickSize(0) - .tickPadding(0) - .ticks(numTicksForDateFormat(cxtWidth, xAxisTickFormat)) - .tickFormat(d => { - return moment(d).format(xAxisTickFormat); - }); - - cxtGroup.datum(data); - const contextBoundsArea = d3.svg - .area() - .x(d => { - return this.contextXScale(d.date); - }) - .y0(d => { - return this.contextYScale(Math.min(chartLimits.max, Math.max(d.lower, chartLimits.min))); - }) - .y1(d => { - return this.contextYScale(Math.max(chartLimits.min, Math.min(d.upper, chartLimits.max))); - }) - .defined(d => d.lower !== null && d.upper !== null); - - if (modelPlotEnabled === true) { - cxtGroup - .append('path') - .datum(data) - .attr('class', 'area context') - .attr('d', contextBoundsArea); + if (valuesRange.max < dataMax) { + chartLimits.max = valuesRange.max + 0.5 * (valuesRange.max - valuesRange.min); + } } + } - const contextValuesLine = d3.svg - .line() - .x(d => { - return this.contextXScale(d.date); - }) - .y(d => { - return this.contextYScale(d.value); - }) - .defined(d => d.value !== null); + this.contextYScale = d3.scale + .linear() + .range([cxtChartHeight, contextChartLineTopMargin]) + .domain([chartLimits.min, chartLimits.max]); + + const borders = cxtGroup.append('g').attr('class', 'axis'); + + // Add borders left and right. + borders + .append('line') + .attr('x1', 0) + .attr('y1', 0) + .attr('x2', 0) + .attr('y2', cxtChartHeight + swlHeight); + borders + .append('line') + .attr('x1', cxtWidth) + .attr('y1', 0) + .attr('x2', cxtWidth) + .attr('y2', cxtChartHeight + swlHeight); + + // Add x axis. + const timeBuckets = new TimeBuckets(); + timeBuckets.setInterval('auto'); + timeBuckets.setBounds(bounds); + const xAxisTickFormat = timeBuckets.getScaledDateFormat(); + const xAxis = d3.svg + .axis() + .scale(this.contextXScale) + .orient('top') + .innerTickSize(-cxtChartHeight) + .outerTickSize(0) + .tickPadding(0) + .ticks(numTicksForDateFormat(cxtWidth, xAxisTickFormat)) + .tickFormat(d => { + return moment(d).format(xAxisTickFormat); + }); + cxtGroup.datum(data); + + const contextBoundsArea = d3.svg + .area() + .x(d => { + return this.contextXScale(d.date); + }) + .y0(d => { + return this.contextYScale(Math.min(chartLimits.max, Math.max(d.lower, chartLimits.min))); + }) + .y1(d => { + return this.contextYScale(Math.max(chartLimits.min, Math.min(d.upper, chartLimits.max))); + }) + .defined(d => d.lower !== null && d.upper !== null); + + if (modelPlotEnabled === true) { cxtGroup .append('path') .datum(data) - .attr('class', 'values-line') - .attr('d', contextValuesLine); - drawLineChartDots(data, cxtGroup, contextValuesLine, 1); - - // Create the path elements for the forecast value line and bounds area. - if (contextForecastData !== undefined) { - cxtGroup - .append('path') - .datum(contextForecastData) - .attr('class', 'area forecast') - .attr('d', contextBoundsArea); - cxtGroup - .append('path') - .datum(contextForecastData) - .attr('class', 'values-line forecast') - .attr('d', contextValuesLine); - } - - // Create and draw the anomaly swimlane. - const swimlane = cxtGroup - .append('g') - .attr('class', 'swimlane') - .attr('transform', 'translate(0,' + cxtChartHeight + ')'); - - this.drawSwimlane(swimlane, cxtWidth, swlHeight); - - // Draw a mask over the sections of the context chart and swimlane - // which fall outside of the zoom brush selection area. - this.mask = new ContextChartMask(cxtGroup, contextChartData, modelPlotEnabled, swlHeight) - .x(this.contextXScale) - .y(this.contextYScale); + .attr('class', 'area context') + .attr('d', contextBoundsArea); + } - // Draw the x axis on top of the mask so that the labels are visible. + const contextValuesLine = d3.svg + .line() + .x(d => { + return this.contextXScale(d.date); + }) + .y(d => { + return this.contextYScale(d.value); + }) + .defined(d => d.value !== null); + + cxtGroup + .append('path') + .datum(data) + .attr('class', 'values-line') + .attr('d', contextValuesLine); + drawLineChartDots(data, cxtGroup, contextValuesLine, 1); + + // Create the path elements for the forecast value line and bounds area. + if (contextForecastData !== undefined) { cxtGroup - .append('g') - .attr('class', 'x axis context-chart-axis') - .call(xAxis); - - // Move the x axis labels up so that they are inside the contact chart area. - cxtGroup.selectAll('.x.context-chart-axis text').attr('dy', cxtChartHeight - 5); - - filterAxisLabels(cxtGroup.selectAll('.x.context-chart-axis'), cxtWidth); - - this.drawContextBrush(cxtGroup); + .append('path') + .datum(contextForecastData) + .attr('class', 'area forecast') + .attr('d', contextBoundsArea); + cxtGroup + .append('path') + .datum(contextForecastData) + .attr('class', 'values-line forecast') + .attr('d', contextValuesLine); } - drawContextBrush = contextGroup => { - const { contextChartSelected } = this.props; - - const brush = this.brush; - const contextXScale = this.contextXScale; - const mask = this.mask; - - // Create the brush for zooming in to the focus area of interest. - brush - .x(contextXScale) - .on('brush', brushing) - .on('brushend', brushed); - - contextGroup - .append('g') - .attr('class', 'x brush') - .call(brush) - .selectAll('rect') - .attr('y', -1) - .attr('height', contextChartHeight + swimlaneHeight + 1); - - // move the left and right resize areas over to - // be under the handles - contextGroup - .selectAll('.w rect') - .attr('x', -10) - .attr('width', 10); - - contextGroup - .selectAll('.e rect') - .attr('x', 0) - .attr('width', 10); - - const handleBrushExtent = brush.extent(); - - const topBorder = contextGroup - .append('rect') - .attr('class', 'top-border') - .attr('y', -2) - .attr('height', contextChartLineTopMargin); - - // Draw the brush handles using SVG foreignObject elements. - // Note these are not supported on IE11 and below, so will not appear in IE. - const leftHandle = contextGroup - .append('foreignObject') - .attr('width', 10) - .attr('height', 90) - .attr('class', 'brush-handle') - .attr('x', contextXScale(handleBrushExtent[0]) - 10) - .html( - '
' - ); - const rightHandle = contextGroup - .append('foreignObject') - .attr('width', 10) - .attr('height', 90) - .attr('class', 'brush-handle') - .attr('x', contextXScale(handleBrushExtent[1]) + 0) - .html( - '
' - ); + // Create and draw the anomaly swimlane. + const swimlane = cxtGroup + .append('g') + .attr('class', 'swimlane') + .attr('transform', 'translate(0,' + cxtChartHeight + ')'); - const showBrush = show => { - if (show === true) { - const brushExtent = brush.extent(); - mask.reveal(brushExtent); - leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); - rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); - - topBorder.attr('x', contextXScale(brushExtent[0]) + 1); - // Use Math.max(0, ...) to make sure we don't end up - // with a negative width which would cause an SVG error. - topBorder.attr( - 'width', - Math.max(0, contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2) - ); - } + this.drawSwimlane(swimlane, cxtWidth, swlHeight); - this.setBrushVisibility(show); - }; + // Draw a mask over the sections of the context chart and swimlane + // which fall outside of the zoom brush selection area. + this.mask = new ContextChartMask(cxtGroup, contextChartData, modelPlotEnabled, swlHeight) + .x(this.contextXScale) + .y(this.contextYScale); - showBrush(!brush.empty()); + // Draw the x axis on top of the mask so that the labels are visible. + cxtGroup + .append('g') + .attr('class', 'x axis context-chart-axis') + .call(xAxis); - function brushing() { - const isEmpty = brush.empty(); - showBrush(!isEmpty); - } - - const that = this; - function brushed() { - const isEmpty = brush.empty(); - - const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); - const selectionMin = selectedBounds[0].getTime(); - const selectionMax = selectedBounds[1].getTime(); + // Move the x axis labels up so that they are inside the contact chart area. + cxtGroup.selectAll('.x.context-chart-axis text').attr('dy', cxtChartHeight - 5); - // Avoid triggering an update if bounds haven't changed - if ( - that.selectedBounds !== undefined && - that.selectedBounds.min.valueOf() === selectionMin && - that.selectedBounds.max.valueOf() === selectionMax - ) { - return; - } + filterAxisLabels(cxtGroup.selectAll('.x.context-chart-axis'), cxtWidth); - showBrush(!isEmpty); + this.drawContextBrush(cxtGroup); + } - // Set the color of the swimlane cells according to whether they are inside the selection. - contextGroup.selectAll('.swimlane-cell').style('fill', d => { - const cellMs = d.date.getTime(); - if (cellMs < selectionMin || cellMs > selectionMax) { - return anomalyGrayScale(d.score); - } else { - return anomalyColorScale(d.score); - } - }); + drawContextBrush = contextGroup => { + const { contextChartSelected } = this.props; + + const brush = this.brush; + const contextXScale = this.contextXScale; + const mask = this.mask; + + // Create the brush for zooming in to the focus area of interest. + brush + .x(contextXScale) + .on('brush', brushing) + .on('brushend', brushed); + + contextGroup + .append('g') + .attr('class', 'x brush') + .call(brush) + .selectAll('rect') + .attr('y', -1) + .attr('height', contextChartHeight + swimlaneHeight + 1); + + // move the left and right resize areas over to + // be under the handles + contextGroup + .selectAll('.w rect') + .attr('x', -10) + .attr('width', 10); + + contextGroup + .selectAll('.e rect') + .attr('x', 0) + .attr('width', 10); + + const handleBrushExtent = brush.extent(); + + const topBorder = contextGroup + .append('rect') + .attr('class', 'top-border') + .attr('y', -2) + .attr('height', contextChartLineTopMargin); + + // Draw the brush handles using SVG foreignObject elements. + // Note these are not supported on IE11 and below, so will not appear in IE. + const leftHandle = contextGroup + .append('foreignObject') + .attr('width', 10) + .attr('height', 90) + .attr('class', 'brush-handle') + .attr('x', contextXScale(handleBrushExtent[0]) - 10) + .html( + '
' + ); + const rightHandle = contextGroup + .append('foreignObject') + .attr('width', 10) + .attr('height', 90) + .attr('class', 'brush-handle') + .attr('x', contextXScale(handleBrushExtent[1]) + 0) + .html( + '
' + ); - that.selectedBounds = { min: moment(selectionMin), max: moment(selectionMax) }; - contextChartSelected({ from: selectedBounds[0], to: selectedBounds[1] }); + const showBrush = show => { + if (show === true) { + const brushExtent = brush.extent(); + mask.reveal(brushExtent); + leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); + rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); + + topBorder.attr('x', contextXScale(brushExtent[0]) + 1); + // Use Math.max(0, ...) to make sure we don't end up + // with a negative width which would cause an SVG error. + topBorder.attr( + 'width', + Math.max(0, contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2) + ); } - }; - setBrushVisibility = show => { - const mask = this.mask; + this.setBrushVisibility(show); + }; - if (mask !== undefined) { - const visibility = show ? 'visible' : 'hidden'; - mask.style('visibility', visibility); + showBrush(!brush.empty()); - d3.selectAll('.brush').style('visibility', visibility); + function brushing() { + const isEmpty = brush.empty(); + showBrush(!isEmpty); + } - const brushHandles = d3.selectAll('.brush-handle-inner'); - brushHandles.style('visibility', visibility); + const that = this; + function brushed() { + const isEmpty = brush.empty(); - const topBorder = d3.selectAll('.top-border'); - topBorder.style('visibility', visibility); + const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); + const selectionMin = selectedBounds[0].getTime(); + const selectionMax = selectedBounds[1].getTime(); - const border = d3.selectAll('.chart-border-highlight'); - border.style('visibility', visibility); + // Avoid triggering an update if bounds haven't changed + if ( + that.selectedBounds !== undefined && + that.selectedBounds.min.valueOf() === selectionMin && + that.selectedBounds.max.valueOf() === selectionMax + ) { + return; } - }; - drawSwimlane = (swlGroup, swlWidth, swlHeight) => { - const { contextAggregationInterval, swimlaneData } = this.props; + showBrush(!isEmpty); - const data = swimlaneData; - - if (typeof data === 'undefined') { - return; - } + // Set the color of the swimlane cells according to whether they are inside the selection. + contextGroup.selectAll('.swimlane-cell').style('fill', d => { + const cellMs = d.date.getTime(); + if (cellMs < selectionMin || cellMs > selectionMax) { + return anomalyGrayScale(d.score); + } else { + return anomalyColorScale(d.score); + } + }); - // Calculate the x axis domain. - // Elasticsearch aggregation returns points at start of bucket, so set the - // x-axis min to the start of the aggregation interval. - // Need to use the min(earliest) and max(earliest) of the context chart - // aggregation to align the axes of the chart and swimlane elements. - const xAxisDomain = this.calculateContextXAxisDomain(); - const x = d3.time - .scale() - .range([0, swlWidth]) - .domain(xAxisDomain); - - const y = d3.scale - .linear() - .range([swlHeight, 0]) - .domain([0, swlHeight]); - - const xAxis = d3.svg - .axis() - .scale(x) - .orient('bottom') - .innerTickSize(-swlHeight) - .outerTickSize(0); - - const yAxis = d3.svg - .axis() - .scale(y) - .orient('left') - .tickValues(y.domain()) - .innerTickSize(-swlWidth) - .outerTickSize(0); - - const axes = swlGroup.append('g'); - - axes - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,' + swlHeight + ')') - .call(xAxis); - - axes - .append('g') - .attr('class', 'y axis') - .call(yAxis); - - const earliest = xAxisDomain[0].getTime(); - const latest = xAxisDomain[1].getTime(); - const swimlaneAggMs = contextAggregationInterval.asMilliseconds(); - let cellWidth = swlWidth / ((latest - earliest) / swimlaneAggMs); - if (cellWidth < 1) { - cellWidth = 1; - } + that.selectedBounds = { min: moment(selectionMin), max: moment(selectionMax) }; + contextChartSelected({ from: selectedBounds[0], to: selectedBounds[1] }); + } + }; - const cells = swlGroup - .append('g') - .attr('class', 'swimlane-cells') - .selectAll('rect') - .data(data); + setBrushVisibility = show => { + const mask = this.mask; - cells - .enter() - .append('rect') - .attr('x', d => { - return x(d.date); - }) - .attr('y', 0) - .attr('rx', 0) - .attr('ry', 0) - .attr('class', d => { - return d.score > 0 ? 'swimlane-cell' : 'swimlane-cell-hidden'; - }) - .attr('width', cellWidth) - .attr('height', swlHeight) - .style('fill', d => { - return anomalyColorScale(d.score); - }); - }; + if (mask !== undefined) { + const visibility = show ? 'visible' : 'hidden'; + mask.style('visibility', visibility); - calculateContextXAxisDomain = () => { - const { bounds, contextAggregationInterval, swimlaneData } = this.props; - // Calculates the x axis domain for the context elements. - // Elasticsearch aggregation returns points at start of bucket, - // so set the x-axis min to the start of the first aggregation interval, - // and the x-axis max to the end of the last aggregation interval. - // Context chart and swimlane use the same aggregation interval. - let earliest = bounds.min.valueOf(); - - if (swimlaneData !== undefined && swimlaneData.length > 0) { - // Adjust the earliest back to the time of the first swimlane point - // if this is before the time filter minimum. - earliest = Math.min(_.first(swimlaneData).date.getTime(), bounds.min.valueOf()); - } + d3.selectAll('.brush').style('visibility', visibility); - const contextAggMs = contextAggregationInterval.asMilliseconds(); - const earliestMs = Math.floor(earliest / contextAggMs) * contextAggMs; - const latestMs = Math.ceil(bounds.max.valueOf() / contextAggMs) * contextAggMs; + const brushHandles = d3.selectAll('.brush-handle-inner'); + brushHandles.style('visibility', visibility); - return [new Date(earliestMs), new Date(latestMs)]; - }; + const topBorder = d3.selectAll('.top-border'); + topBorder.style('visibility', visibility); - // Sets the extent of the brush on the context chart to the - // supplied from and to Date objects. - setContextBrushExtent = (from, to, fireEvent) => { - const brush = this.brush; - const brushExtent = brush.extent(); + const border = d3.selectAll('.chart-border-highlight'); + border.style('visibility', visibility); + } + }; - const newExtent = [from, to]; - if ( - newExtent[0].getTime() === brushExtent[0].getTime() && - newExtent[1].getTime() === brushExtent[1].getTime() - ) { - fireEvent = false; - } + drawSwimlane = (swlGroup, swlWidth, swlHeight) => { + const { contextAggregationInterval, swimlaneData } = this.props; - brush.extent(newExtent); - brush(d3.select('.brush')); - if (fireEvent) { - brush.event(d3.select('.brush')); - } - }; + const data = swimlaneData; - setZoomInterval(ms) { - const { bounds, zoomTo } = this.props; + if (typeof data === 'undefined') { + return; + } - const minBoundsMs = bounds.min.valueOf(); - const maxBoundsMs = bounds.max.valueOf(); + // Calculate the x axis domain. + // Elasticsearch aggregation returns points at start of bucket, so set the + // x-axis min to the start of the aggregation interval. + // Need to use the min(earliest) and max(earliest) of the context chart + // aggregation to align the axes of the chart and swimlane elements. + const xAxisDomain = this.calculateContextXAxisDomain(); + const x = d3.time + .scale() + .range([0, swlWidth]) + .domain(xAxisDomain); + + const y = d3.scale + .linear() + .range([swlHeight, 0]) + .domain([0, swlHeight]); + + const xAxis = d3.svg + .axis() + .scale(x) + .orient('bottom') + .innerTickSize(-swlHeight) + .outerTickSize(0); + + const yAxis = d3.svg + .axis() + .scale(y) + .orient('left') + .tickValues(y.domain()) + .innerTickSize(-swlWidth) + .outerTickSize(0); + + const axes = swlGroup.append('g'); + + axes + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + swlHeight + ')') + .call(xAxis); + + axes + .append('g') + .attr('class', 'y axis') + .call(yAxis); + + const earliest = xAxisDomain[0].getTime(); + const latest = xAxisDomain[1].getTime(); + const swimlaneAggMs = contextAggregationInterval.asMilliseconds(); + let cellWidth = swlWidth / ((latest - earliest) / swimlaneAggMs); + if (cellWidth < 1) { + cellWidth = 1; + } - // Attempt to retain the same zoom end time. - // If not, go back to the bounds start and add on the required millis. - const millis = +ms; - let to = zoomTo.getTime(); - let from = to - millis; - if (from < minBoundsMs) { - from = minBoundsMs; - to = Math.min(minBoundsMs + millis, maxBoundsMs); - } + const cells = swlGroup + .append('g') + .attr('class', 'swimlane-cells') + .selectAll('rect') + .data(data); + + cells + .enter() + .append('rect') + .attr('x', d => { + return x(d.date); + }) + .attr('y', 0) + .attr('rx', 0) + .attr('ry', 0) + .attr('class', d => { + return d.score > 0 ? 'swimlane-cell' : 'swimlane-cell-hidden'; + }) + .attr('width', cellWidth) + .attr('height', swlHeight) + .style('fill', d => { + return anomalyColorScale(d.score); + }); + }; + + calculateContextXAxisDomain = () => { + const { bounds, contextAggregationInterval, swimlaneData } = this.props; + // Calculates the x axis domain for the context elements. + // Elasticsearch aggregation returns points at start of bucket, + // so set the x-axis min to the start of the first aggregation interval, + // and the x-axis max to the end of the last aggregation interval. + // Context chart and swimlane use the same aggregation interval. + let earliest = bounds.min.valueOf(); + + if (swimlaneData !== undefined && swimlaneData.length > 0) { + // Adjust the earliest back to the time of the first swimlane point + // if this is before the time filter minimum. + earliest = Math.min(_.first(swimlaneData).date.getTime(), bounds.min.valueOf()); + } - this.setContextBrushExtent(new Date(from), new Date(to), true); + const contextAggMs = contextAggregationInterval.asMilliseconds(); + const earliestMs = Math.floor(earliest / contextAggMs) * contextAggMs; + const latestMs = Math.ceil(bounds.max.valueOf() / contextAggMs) * contextAggMs; + + return [new Date(earliestMs), new Date(latestMs)]; + }; + + // Sets the extent of the brush on the context chart to the + // supplied from and to Date objects. + setContextBrushExtent = (from, to, fireEvent) => { + const brush = this.brush; + const brushExtent = brush.extent(); + + const newExtent = [from, to]; + if ( + newExtent[0].getTime() === brushExtent[0].getTime() && + newExtent[1].getTime() === brushExtent[1].getTime() + ) { + fireEvent = false; } - showFocusChartTooltip(marker, circle) { - const { modelPlotEnabled, intl } = this.props; + brush.extent(newExtent); + brush(d3.select('.brush')); + if (fireEvent) { + brush.event(d3.select('.brush')); + } + }; + + setZoomInterval(ms) { + const { bounds, zoomTo } = this.props; + + const minBoundsMs = bounds.min.valueOf(); + const maxBoundsMs = bounds.max.valueOf(); + + // Attempt to retain the same zoom end time. + // If not, go back to the bounds start and add on the required millis. + const millis = +ms; + let to = zoomTo.getTime(); + let from = to - millis; + if (from < minBoundsMs) { + from = minBoundsMs; + to = Math.min(minBoundsMs + millis, maxBoundsMs); + } - const fieldFormat = this.fieldFormat; - const seriesKey = 'single_metric_viewer'; + this.setContextBrushExtent(new Date(from), new Date(to), true); + } - // Show the time and metric values in the tooltip. - // Uses date, value, upper, lower and anomalyScore (optional) marker properties. - const formattedDate = formatHumanReadableDateTimeSeconds(marker.date); - const tooltipData = [{ name: formattedDate }]; + showFocusChartTooltip(marker, circle) { + const { modelPlotEnabled } = this.props; + + const fieldFormat = this.fieldFormat; + const seriesKey = 'single_metric_viewer'; + + // Show the time and metric values in the tooltip. + // Uses date, value, upper, lower and anomalyScore (optional) marker properties. + const formattedDate = formatHumanReadableDateTimeSeconds(marker.date); + const tooltipData = [{ name: formattedDate }]; + + if (_.has(marker, 'anomalyScore')) { + const score = parseInt(marker.anomalyScore); + const displayScore = score > 0 ? score : '< 1'; + tooltipData.push({ + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.anomalyScoreLabel', { + defaultMessage: 'anomaly score', + }), + value: displayScore, + color: anomalyColorScale(score), + seriesKey, + yAccessor: 'anomaly_score', + }); - if (_.has(marker, 'anomalyScore')) { - const score = parseInt(marker.anomalyScore); - const displayScore = score > 0 ? score : '< 1'; + if (showMultiBucketAnomalyTooltip(marker) === true) { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.anomalyScoreLabel', - defaultMessage: 'anomaly score', - }), - value: displayScore, - color: anomalyColorScale(score), + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel', + { + defaultMessage: 'multi-bucket impact', + } + ), + value: getMultiBucketImpactLabel(marker.multiBucketImpact), seriesKey, - yAccessor: 'anomaly_score', + yAccessor: 'multi_bucket_impact', }); + } - if (showMultiBucketAnomalyTooltip(marker) === true) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel', - defaultMessage: 'multi-bucket impact', - }), - value: getMultiBucketImpactLabel(marker.multiBucketImpact), - seriesKey, - yAccessor: 'multi_bucket_impact', - }); - } - - if (modelPlotEnabled === false) { - // Show actual/typical when available except for rare detectors. - // Rare detectors always have 1 as actual and the probability as typical. - // Exposing those values in the tooltip with actual/typical labels might irritate users. - if (_.has(marker, 'actual') && marker.function !== 'rare') { - // Display the record actual in preference to the chart value, which may be - // different depending on the aggregation interval of the chart. - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.actualLabel', - defaultMessage: 'actual', - }), - value: formatValue(marker.actual, marker.function, fieldFormat), - seriesKey, - yAccessor: 'actual', - }); - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.typicalLabel', - defaultMessage: 'typical', - }), - value: formatValue(marker.typical, marker.function, fieldFormat), - seriesKey, - yAccessor: 'typical', - }); - } else { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel', - defaultMessage: 'value', - }), - value: formatValue(marker.value, marker.function, fieldFormat), - seriesKey, - yAccessor: 'value', - }); - if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { - const numberOfCauses = marker.numberOfCauses; - // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. - const byFieldName = mlEscape(marker.byFieldName); - tooltipData.push({ - name: intl.formatMessage( - { - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.moreThanOneUnusualByFieldValuesLabel', - defaultMessage: '{numberOfCauses}{plusSign} unusual {byFieldName} values', - }, - { - numberOfCauses, - byFieldName, - // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. - plusSign: numberOfCauses < 10 ? '' : '+', - } - ), - seriesKey, - yAccessor: 'numberOfCauses', - }); - } - } - } else { + if (modelPlotEnabled === false) { + // Show actual/typical when available except for rare detectors. + // Rare detectors always have 1 as actual and the probability as typical. + // Exposing those values in the tooltip with actual/typical labels might irritate users. + if (_.has(marker, 'actual') && marker.function !== 'rare') { + // Display the record actual in preference to the chart value, which may be + // different depending on the aggregation interval of the chart. tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.actualLabel', + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.actualLabel', { defaultMessage: 'actual', }), value: formatValue(marker.actual, marker.function, fieldFormat), @@ -1528,212 +1460,269 @@ const TimeseriesChartIntl = injectI18n( yAccessor: 'actual', }); tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel', - defaultMessage: 'upper bounds', + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.typicalLabel', { + defaultMessage: 'typical', }), - value: formatValue(marker.upper, marker.function, fieldFormat), + value: formatValue(marker.typical, marker.function, fieldFormat), seriesKey, - yAccessor: 'upper_bounds', - }); - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel', - defaultMessage: 'lower bounds', - }), - value: formatValue(marker.lower, marker.function, fieldFormat), - seriesKey, - yAccessor: 'lower_bounds', - }); - } - } else { - // TODO - need better formatting for small decimals. - if (_.get(marker, 'isForecast', false) === true) { - tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.predictionLabel', - defaultMessage: 'prediction', - }), - value: formatValue(marker.value, marker.function, fieldFormat), - seriesKey, - yAccessor: 'prediction', + yAccessor: 'typical', }); } else { tooltipData.push({ - name: intl.formatMessage({ - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.valueLabel', + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel', { defaultMessage: 'value', }), value: formatValue(marker.value, marker.function, fieldFormat), seriesKey, yAccessor: 'value', }); + if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) { + const numberOfCauses = marker.numberOfCauses; + // If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields. + const byFieldName = mlEscape(marker.byFieldName); + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.moreThanOneUnusualByFieldValuesLabel', + { + defaultMessage: '{numberOfCauses}{plusSign} unusual {byFieldName} values', + values: { + numberOfCauses, + byFieldName, + // Maximum of 10 causes are stored in the record, so '10' may mean more than 10. + plusSign: numberOfCauses < 10 ? '' : '+', + }, + } + ), + seriesKey, + yAccessor: 'numberOfCauses', + }); + } } - - if (modelPlotEnabled === true) { - tooltipData.push({ - name: intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.upperBoundsLabel', + } else { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.actualLabel', + { + defaultMessage: 'actual', + } + ), + value: formatValue(marker.actual, marker.function, fieldFormat), + seriesKey, + yAccessor: 'actual', + }); + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel', + { defaultMessage: 'upper bounds', - }), - value: formatValue(marker.upper, marker.function, fieldFormat), - seriesKey, - yAccessor: 'upper_bounds', - }); - tooltipData.push({ - name: intl.formatMessage({ - id: - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.lowerBoundsLabel', + } + ), + value: formatValue(marker.upper, marker.function, fieldFormat), + seriesKey, + yAccessor: 'upper_bounds', + }); + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel', + { defaultMessage: 'lower bounds', - }), - value: formatValue(marker.lower, marker.function, fieldFormat), - seriesKey, - yAccessor: 'lower_bounds', - }); - } + } + ), + value: formatValue(marker.lower, marker.function, fieldFormat), + seriesKey, + yAccessor: 'lower_bounds', + }); } - - if (_.has(marker, 'scheduledEvents')) { - marker.scheduledEvents.forEach((scheduledEvent, i) => { - tooltipData.push({ - name: intl.formatMessage( - { - id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel', - defaultMessage: 'scheduled event{counter}', - }, - { counter: marker.scheduledEvents.length > 1 ? ` #${i + 1}` : '' } - ), - value: scheduledEvent, - seriesKey, - yAccessor: `scheduled_events_${i + 1}`, - }); + } else { + // TODO - need better formatting for small decimals. + if (_.get(marker, 'isForecast', false) === true) { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.predictionLabel', + { + defaultMessage: 'prediction', + } + ), + value: formatValue(marker.value, marker.function, fieldFormat), + seriesKey, + yAccessor: 'prediction', + }); + } else { + tooltipData.push({ + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.valueLabel', + { + defaultMessage: 'value', + } + ), + value: formatValue(marker.value, marker.function, fieldFormat), + seriesKey, + yAccessor: 'value', }); } - if (mlAnnotationsEnabled && _.has(marker, 'annotation')) { - tooltipData.length = 0; + if (modelPlotEnabled === true) { tooltipData.push({ - name: marker.annotation, + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.upperBoundsLabel', + { + defaultMessage: 'upper bounds', + } + ), + value: formatValue(marker.upper, marker.function, fieldFormat), + seriesKey, + yAccessor: 'upper_bounds', }); - let timespan = moment(marker.timestamp).format('MMMM Do YYYY, HH:mm'); - - if (typeof marker.end_timestamp !== 'undefined') { - timespan += ` - ${moment(marker.end_timestamp).format('MMMM Do YYYY, HH:mm')}`; - } tooltipData.push({ - name: timespan, + name: i18n.translate( + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.lowerBoundsLabel', + { + defaultMessage: 'lower bounds', + } + ), + value: formatValue(marker.lower, marker.function, fieldFormat), + seriesKey, + yAccessor: 'lower_bounds', }); } + } - let xOffset = LINE_CHART_ANOMALY_RADIUS * 2; + if (_.has(marker, 'scheduledEvents')) { + marker.scheduledEvents.forEach((scheduledEvent, i) => { + tooltipData.push({ + name: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel', { + defaultMessage: 'scheduled event{counter}', + values: { + counter: marker.scheduledEvents.length > 1 ? ` #${i + 1}` : '', + }, + }), + value: scheduledEvent, + seriesKey, + yAccessor: `scheduled_events_${i + 1}`, + }); + }); + } - // When the annotation area is hovered - if (circle.tagName.toLowerCase() === 'rect') { - const x = Number(circle.getAttribute('x')); - if (x < 0) { - // The beginning of the annotation area is outside of the focus chart, - // hence we need to adjust the x offset of a tooltip. - xOffset = Math.abs(x); - } - } + if (_.has(marker, 'annotation')) { + tooltipData.length = 0; + tooltipData.push({ + name: marker.annotation, + }); + let timespan = moment(marker.timestamp).format('MMMM Do YYYY, HH:mm'); - mlChartTooltipService.show(tooltipData, circle, { - x: xOffset, - y: 0, + if (typeof marker.end_timestamp !== 'undefined') { + timespan += ` - ${moment(marker.end_timestamp).format('MMMM Do YYYY, HH:mm')}`; + } + tooltipData.push({ + name: timespan, }); } - highlightFocusChartAnomaly(record) { - // Highlights the anomaly marker in the focus chart corresponding to the specified record. - - const { focusChartData, focusAggregationInterval } = this.props; + let xOffset = LINE_CHART_ANOMALY_RADIUS * 2; - const focusXScale = this.focusXScale; - const focusYScale = this.focusYScale; - const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + // When the annotation area is hovered + if (circle.tagName.toLowerCase() === 'rect') { + const x = Number(circle.getAttribute('x')); + if (x < 0) { + // The beginning of the annotation area is outside of the focus chart, + // hence we need to adjust the x offset of a tooltip. + xOffset = Math.abs(x); + } + } - // Find the anomaly marker which corresponds to the time of the anomaly record. - // Depending on the way the chart is aggregated, there may not be - // a point at exactly the same time as the record being highlighted. - const anomalyTime = record.source.timestamp; - const markerToSelect = findChartPointForAnomalyTime( - focusChartData, - anomalyTime, - focusAggregationInterval - ); + mlChartTooltipService.show(tooltipData, circle, { + x: xOffset, + y: 0, + }); + } - // Render an additional highlighted anomaly marker on the focus chart. - // TODO - plot anomaly markers for cases where there is an anomaly due - // to the absence of data and model plot is enabled. - if (markerToSelect !== undefined) { - const selectedMarker = d3 - .select('.focus-chart-markers') - .selectAll('.focus-chart-highlighted-marker') - .data([markerToSelect]); - if (showMultiBucketAnomalyMarker(markerToSelect) === true) { - selectedMarker - .enter() - .append('path') - .attr( - 'd', - d3.svg - .symbol() - .size(MULTI_BUCKET_SYMBOL_SIZE) - .type('cross') - ) - .attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`) - .attr( - 'class', - d => - `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id} highlighted` - ); - } else { - selectedMarker - .enter() - .append('circle') - .attr('r', LINE_CHART_ANOMALY_RADIUS) - .attr('cx', d => focusXScale(d.date)) - .attr('cy', d => focusYScale(d.value)) - .attr( - 'class', - d => - `anomaly-marker metric-value ${getSeverityWithLow(d.anomalyScore).id} highlighted` - ); - } + highlightFocusChartAnomaly(record) { + // Highlights the anomaly marker in the focus chart corresponding to the specified record. + + const { focusChartData, focusAggregationInterval } = this.props; + + const focusXScale = this.focusXScale; + const focusYScale = this.focusYScale; + const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + + // Find the anomaly marker which corresponds to the time of the anomaly record. + // Depending on the way the chart is aggregated, there may not be + // a point at exactly the same time as the record being highlighted. + const anomalyTime = record.source.timestamp; + const markerToSelect = findChartPointForAnomalyTime( + focusChartData, + anomalyTime, + focusAggregationInterval + ); + + // Render an additional highlighted anomaly marker on the focus chart. + // TODO - plot anomaly markers for cases where there is an anomaly due + // to the absence of data and model plot is enabled. + if (markerToSelect !== undefined) { + const selectedMarker = d3 + .select('.focus-chart-markers') + .selectAll('.focus-chart-highlighted-marker') + .data([markerToSelect]); + if (showMultiBucketAnomalyMarker(markerToSelect) === true) { + selectedMarker + .enter() + .append('path') + .attr( + 'd', + d3.svg + .symbol() + .size(MULTI_BUCKET_SYMBOL_SIZE) + .type('cross') + ) + .attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`) + .attr( + 'class', + d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id} highlighted` + ); + } else { + selectedMarker + .enter() + .append('circle') + .attr('r', LINE_CHART_ANOMALY_RADIUS) + .attr('cx', d => focusXScale(d.date)) + .attr('cy', d => focusYScale(d.value)) + .attr( + 'class', + d => `anomaly-marker metric-value ${getSeverityWithLow(d.anomalyScore).id} highlighted` + ); + } - // Display the chart tooltip for this marker. - // Note the values of the record and marker may differ depending on the levels of aggregation. - const chartElement = d3.select(this.rootNode); - const anomalyMarker = chartElement.selectAll( - '.focus-chart-markers .anomaly-marker.highlighted' - ); - if (anomalyMarker.length) { - showFocusChartTooltip(markerToSelect, anomalyMarker[0][0]); - } + // Display the chart tooltip for this marker. + // Note the values of the record and marker may differ depending on the levels of aggregation. + const chartElement = d3.select(this.rootNode); + const anomalyMarker = chartElement.selectAll( + '.focus-chart-markers .anomaly-marker.highlighted' + ); + if (anomalyMarker.length) { + showFocusChartTooltip(markerToSelect, anomalyMarker[0][0]); } } + } - unhighlightFocusChartAnomaly() { - d3.select('.focus-chart-markers') - .selectAll('.anomaly-marker.highlighted') - .remove(); - mlChartTooltipService.hide(); - } + unhighlightFocusChartAnomaly() { + d3.select('.focus-chart-markers') + .selectAll('.anomaly-marker.highlighted') + .remove(); + mlChartTooltipService.hide(); + } - shouldComponentUpdate() { - return true; - } + shouldComponentUpdate() { + return true; + } - setRef(componentNode) { - this.rootNode = componentNode; - } + setRef(componentNode) { + this.rootNode = componentNode; + } - render() { - return
; - } + render() { + return
; } -); +} export const TimeseriesChart = props => { const annotationProp = useObservable(annotation$); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js index cc77ad9f1a985..784ab102fd8ca 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js @@ -6,25 +6,12 @@ //import mockOverallSwimlaneData from './__mocks__/mock_overall_swimlane.json'; -import './timeseries_chart.test.mocks'; import moment from 'moment-timezone'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { TimeseriesChart } from './timeseries_chart'; -// mocking the following files because they import some core kibana -// code which the jest setup isn't happy with. -jest.mock('ui/chrome', () => ({ - addBasePath: path => path, - getBasePath: path => path, - // returns false for mlAnnotationsEnabled - getInjected: () => false, - getUiSettingsClient: () => ({ - get: jest.fn(), - }), -})); - jest.mock('../../../util/time_buckets', () => ({ TimeBuckets: function() { this.setBounds = jest.fn(); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts deleted file mode 100644 index 46178a7d02977..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('ui/timefilter', () => { - return {}; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts index 4253316123f01..cb66b8d53e660 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -6,8 +6,6 @@ import { FC } from 'react'; -import { Timefilter } from 'ui/timefilter'; - import { getDateFormatTz, TimeRangeBounds } from '../explorer/explorer_utils'; declare const TimeSeriesExplorer: FC<{ diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 6d9dbef64b009..ce52609f6d74f 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -30,8 +30,7 @@ import { EuiTitle, } from '@elastic/eui'; -import chrome from 'ui/chrome'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../util/dependency_cache'; import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/public'; import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; @@ -80,8 +79,6 @@ import { getFocusData, } from './timeseriesexplorer_utils'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); - // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be // obtained as it is not aggregatable e.g. 'all distinct kpi_indicator values' @@ -135,8 +132,8 @@ function getTimeseriesexplorerDefaultState() { loading: false, modelPlotEnabled: false, // Toggles display of annotations in the focus chart - showAnnotations: mlAnnotationsEnabled, - showAnnotationsCheckbox: mlAnnotationsEnabled, + showAnnotations: true, + showAnnotationsCheckbox: true, // Toggles display of forecast data in the focus chart showForecast: true, showForecastCheckbox: false, @@ -216,11 +213,9 @@ export class TimeSeriesExplorer extends React.Component { }; toggleShowAnnotationsHandler = () => { - if (mlAnnotationsEnabled) { - this.setState(prevState => ({ - showAnnotations: !prevState.showAnnotations, - })); - } + this.setState(prevState => ({ + showAnnotations: !prevState.showAnnotations, + })); }; toggleShowForecastHandler = () => { @@ -815,6 +810,7 @@ export class TimeSeriesExplorer extends React.Component { }, } ); + const toastNotifications = getToastNotifications(); toastNotifications.addWarning(warningText); detectorIndex = detectors[0].index; } diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index 03fe718de9bed..2a4eaf1a545a1 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -6,7 +6,6 @@ import { forkJoin, Observable, of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; -import chrome from 'ui/chrome'; import { ml } from '../../services/ml_api_service'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, @@ -26,8 +25,6 @@ import { mlForecastService } from '../../services/forecast_service'; import { mlFunctionToESAggregation } from '../../../../common/util/job_utils'; import { Annotation } from '../../../../common/types/annotations'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); - export interface Interval { asMilliseconds: () => number; expression: string; @@ -81,21 +78,19 @@ export function getFocusData( MAX_SCHEDULED_EVENTS ), // Query 4 - load any annotations for the selected job. - mlAnnotationsEnabled - ? ml.annotations - .getAnnotations({ - jobIds: [selectedJob.job_id], - earliestMs: searchBounds.min.valueOf(), - latestMs: searchBounds.max.valueOf(), - maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, - }) - .pipe( - catchError(() => { - // silent fail - return of({ annotations: {} as Record }); - }) - ) - : of(null), + ml.annotations + .getAnnotations({ + jobIds: [selectedJob.job_id], + earliestMs: searchBounds.min.valueOf(), + latestMs: searchBounds.max.valueOf(), + maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + }) + .pipe( + catchError(() => { + // silent fail + return of({ annotations: {} as Record }); + }) + ), // Plus query for forecast data if there is a forecastId stored in the appState. forecastId !== undefined ? (() => { diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts index f1cdaf3ba8c1b..bd8f98e0428a1 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts @@ -8,7 +8,7 @@ import { difference, without } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getToastNotifications } from '../../util/dependency_cache'; import { MlJobWithTimeRange } from '../../../../common/types/jobs'; @@ -26,6 +26,7 @@ export function validateJobSelection( selectedJobIds: string[], setGlobalState: (...args: any) => void ) { + const toastNotifications = getToastNotifications(); const jobs = createTimeSeriesJobData(mlJobService.jobs); const timeSeriesJobIds: string[] = jobs.map((j: any) => j.id); diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js index dfa896b3124c6..568d078ae03b1 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js @@ -10,7 +10,7 @@ import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impa import moment from 'moment'; import rison from 'rison-node'; -import { timefilter } from 'ui/timefilter'; +import { getTimefilter } from '../util/dependency_cache'; import { CHART_TYPE } from '../explorer/explorer_constants'; @@ -180,6 +180,7 @@ export function getChartType(config) { export function getExploreSeriesLink(series) { // Open the Single Metric dashboard over the same overall bounds and // zoomed in to the same time as the current chart. + const timefilter = getTimefilter(); const bounds = timefilter.getActiveBounds(); const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z const to = bounds.max.toISOString(); diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js index 437f71acb3376..4b33cb131be7f 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js @@ -6,7 +6,7 @@ import seriesConfig from '../explorer/explorer_charts/__mocks__/mock_series_config_filebeat'; -jest.mock('ui/timefilter', () => { +jest.mock('./dependency_cache', () => { const dateMath = require('@elastic/datemath'); let _time = undefined; const timefilter = { @@ -21,23 +21,11 @@ jest.mock('ui/timefilter', () => { }, }; return { - timefilter, + getTimefilter: () => timefilter, }; }); -import { timefilter } from 'ui/timefilter'; - -// A copy of these mocks for ui/chrome and ui/timefilter are also -// used in explorer_charts_container.test.js. -// TODO: Refactor the involved tests to avoid this duplication -jest.mock( - 'ui/chrome', - () => ({ - getBasePath: () => { - return ''; - }, - }), - { virtual: true } -); +import { getTimefilter } from './dependency_cache'; +const timefilter = getTimefilter(); import d3 from 'd3'; import moment from 'moment'; diff --git a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts new file mode 100644 index 0000000000000..52db6560b67f1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts @@ -0,0 +1,201 @@ +/* + * 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 { TimefilterSetup } from 'src/plugins/data/public'; +import { + IUiSettingsClient, + ChromeStart, + SavedObjectsClientContract, + ApplicationStart, + HttpStart, +} from 'src/core/public'; +import { + IndexPatternsContract, + FieldFormatsStart, + DataPublicPluginStart, +} from 'src/plugins/data/public'; +import { + DocLinksStart, + ToastsStart, + OverlayStart, + ChromeRecentlyAccessed, + IBasePath, +} from 'kibana/public'; + +export interface DependencyCache { + timefilter: TimefilterSetup | null; + config: IUiSettingsClient | null; + indexPatterns: IndexPatternsContract | null; + chrome: ChromeStart | null; + docLinks: DocLinksStart | null; + toastNotifications: ToastsStart | null; + overlays: OverlayStart | null; + recentlyAccessed: ChromeRecentlyAccessed | null; + fieldFormats: FieldFormatsStart | null; + autocomplete: DataPublicPluginStart['autocomplete'] | null; + basePath: IBasePath | null; + savedObjectsClient: SavedObjectsClientContract | null; + XSRF: string | null; + APP_URL: string | null; + application: ApplicationStart | null; + http: HttpStart | null; +} + +const cache: DependencyCache = { + timefilter: null, + config: null, + indexPatterns: null, + chrome: null, + docLinks: null, + toastNotifications: null, + overlays: null, + recentlyAccessed: null, + fieldFormats: null, + autocomplete: null, + basePath: null, + savedObjectsClient: null, + XSRF: null, + APP_URL: null, + application: null, + http: null, +}; + +export function setDependencyCache(deps: Partial) { + cache.timefilter = deps.timefilter || null; + cache.config = deps.config || null; + cache.chrome = deps.chrome || null; + cache.indexPatterns = deps.indexPatterns || null; + cache.docLinks = deps.docLinks || null; + cache.toastNotifications = deps.toastNotifications || null; + cache.overlays = deps.overlays || null; + cache.recentlyAccessed = deps.recentlyAccessed || null; + cache.fieldFormats = deps.fieldFormats || null; + cache.autocomplete = deps.autocomplete || null; + cache.basePath = deps.basePath || null; + cache.savedObjectsClient = deps.savedObjectsClient || null; + cache.XSRF = deps.XSRF || null; + cache.APP_URL = deps.APP_URL || null; + cache.application = deps.application || null; + cache.http = deps.http || null; +} + +export function getTimefilter() { + if (cache.timefilter === null) { + throw new Error("timefilter hasn't been initialized"); + } + return cache.timefilter.timefilter; +} +export function getTimeHistory() { + if (cache.timefilter === null) { + throw new Error("timefilter hasn't been initialized"); + } + return cache.timefilter.history; +} + +export function getDocLinks() { + if (cache.docLinks === null) { + throw new Error("docLinks hasn't been initialized"); + } + return cache.docLinks; +} + +export function getToastNotifications() { + if (cache.toastNotifications === null) { + throw new Error("toast notifications haven't been initialized"); + } + return cache.toastNotifications; +} + +export function getOverlays() { + if (cache.overlays === null) { + throw new Error("overlays haven't been initialized"); + } + return cache.overlays; +} + +export function getUiSettings() { + if (cache.config === null) { + throw new Error("uiSettings hasn't been initialized"); + } + return cache.config; +} + +export function getRecentlyAccessed() { + if (cache.recentlyAccessed === null) { + throw new Error("recentlyAccessed hasn't been initialized"); + } + return cache.recentlyAccessed; +} + +export function getFieldFormats() { + if (cache.fieldFormats === null) { + throw new Error("fieldFormats hasn't been initialized"); + } + return cache.fieldFormats; +} + +export function getAutocomplete() { + if (cache.autocomplete === null) { + throw new Error("autocomplete hasn't been initialized"); + } + return cache.autocomplete; +} + +export function getChrome() { + if (cache.chrome === null) { + throw new Error("chrome hasn't been initialized"); + } + return cache.chrome; +} + +export function getBasePath() { + if (cache.basePath === null) { + throw new Error("basePath hasn't been initialized"); + } + return cache.basePath; +} + +export function getSavedObjectsClient() { + if (cache.savedObjectsClient === null) { + throw new Error("savedObjectsClient hasn't been initialized"); + } + return cache.savedObjectsClient; +} + +export function getXSRF() { + if (cache.XSRF === null) { + throw new Error("xsrf hasn't been initialized"); + } + return cache.XSRF; +} + +export function getAppUrl() { + if (cache.APP_URL === null) { + throw new Error("app url hasn't been initialized"); + } + return cache.APP_URL; +} + +export function getApplication() { + if (cache.application === null) { + throw new Error("application hasn't been initialized"); + } + return cache.application; +} + +export function getHttp() { + if (cache.http === null) { + throw new Error("http hasn't been initialized"); + } + return cache.http; +} + +export function clearCache() { + console.log('clearing dependency cache'); // eslint-disable-line no-console + Object.keys(cache).forEach(k => { + cache[k as keyof DependencyCache] = null; + }); +} diff --git a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts index 2e176b0044314..88b56b2329ae6 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; import { Query } from 'src/plugins/data/public'; import { IndexPattern, IIndexPattern, IndexPatternsContract, } from '../../../../../../../src/plugins/data/public'; +import { getToastNotifications, getSavedObjectsClient } from './dependency_cache'; import { IndexPatternSavedObject, SavedSearchSavedObject } from '../../../common/types/kibana'; let indexPatternCache: IndexPatternSavedObject[] = []; @@ -21,7 +20,7 @@ let indexPatternsContract: IndexPatternsContract | null = null; export function loadIndexPatterns(indexPatterns: IndexPatternsContract) { indexPatternsContract = indexPatterns; - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); return savedObjectsClient .find({ type: 'index-pattern', @@ -35,7 +34,7 @@ export function loadIndexPatterns(indexPatterns: IndexPatternsContract) { } export function loadSavedSearches() { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); return savedObjectsClient .find({ type: 'search', @@ -48,7 +47,7 @@ export function loadSavedSearches() { } export async function loadSavedSearchById(id: string) { - const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectsClient = getSavedObjectsClient(); const ss = await savedObjectsClient.get('search', id); return ss.error === undefined ? ss : null; } @@ -122,6 +121,7 @@ export function getSavedSearchById(id: string): SavedSearchSavedObject | undefin export function timeBasedIndexCheck(indexPattern: IndexPattern, showNotification = false) { if (!indexPattern.isTimeBased()) { if (showNotification) { + const toastNotifications = getToastNotifications(); toastNotifications.addWarning({ title: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationTitle', { defaultMessage: 'The index pattern {indexPatternTitle} is not based on a time series', diff --git a/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts b/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts index 196d24bfff830..ab879e421cb09 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts @@ -6,9 +6,10 @@ // utility functions for managing which links get added to kibana's recently accessed list -import { npStart } from 'ui/new_platform'; import { i18n } from '@kbn/i18n'; +import { getRecentlyAccessed } from './dependency_cache'; + export function addItemToRecentlyAccessed(page: string, itemId: string, url: string) { let pageLabel = ''; let id = `ml-job-${itemId}`; @@ -37,6 +38,6 @@ export function addItemToRecentlyAccessed(page: string, itemId: string, url: str } url = `ml#/${page}/${url}`; - - npStart.core.chrome.recentlyAccessed.add(url, `ML - ${itemId} - ${pageLabel}`, id); + const recentlyAccessed = getRecentlyAccessed(); + recentlyAccessed.add(url, `ML - ${itemId} - ${pageLabel}`, id); } diff --git a/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js index 2ac6f7dbd2fb5..ec1b8c842d204 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js +++ b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js @@ -7,20 +7,15 @@ import _ from 'lodash'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import chrome from 'ui/chrome'; -import { npStart } from 'ui/new_platform'; import { timeBucketsCalcAutoIntervalProvider } from './calc_auto_interval'; import { parseInterval } from '../../../common/util/parse_interval'; +import { getFieldFormats, getUiSettings } from './dependency_cache'; import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/data/public'; const unitsDesc = dateMath.unitsDesc; const largeMax = unitsDesc.indexOf('w'); // Multiple units of week or longer converted to days for ES intervals. -const config = chrome.getUiSettingsClient(); - -const getConfig = (...args) => config.get(...args); - const calcAuto = timeBucketsCalcAutoIntervalProvider(); /** @@ -29,8 +24,9 @@ const calcAuto = timeBucketsCalcAutoIntervalProvider(); * for example the interval between points on a time series chart. */ export function TimeBuckets() { - this.barTarget = config.get('histogram:barTarget'); - this.maxBars = config.get('histogram:maxBars'); + const uiSettings = getUiSettings(); + this.barTarget = uiSettings.get('histogram:barTarget'); + this.maxBars = uiSettings.get('histogram:maxBars'); } /** @@ -301,8 +297,9 @@ TimeBuckets.prototype.getIntervalToNearestMultiple = function(divisorSecs) { * @return {string} */ TimeBuckets.prototype.getScaledDateFormat = function() { + const uiSettings = getUiSettings(); const interval = this.getInterval(); - const rules = config.get('dateFormat:scaled'); + const rules = uiSettings.get('dateFormat:scaled'); for (let i = rules.length - 1; i >= 0; i--) { const rule = rules[i]; @@ -311,17 +308,19 @@ TimeBuckets.prototype.getScaledDateFormat = function() { } } - return config.get('dateFormat'); + return uiSettings.get('dateFormat'); }; TimeBuckets.prototype.getScaledDateFormatter = function() { - const fieldFormats = npStart.plugins.data.fieldFormats; + const fieldFormats = getFieldFormats(); + const uiSettings = getUiSettings(); const DateFieldFormat = fieldFormats.getType(FIELD_FORMAT_IDS.DATE); return new DateFieldFormat( { pattern: this.getScaledDateFormat(), }, - getConfig + // getConfig + uiSettings.get ); }; diff --git a/x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.test.js similarity index 55% rename from x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js rename to x-pack/legacy/plugins/ml/public/application/util/time_buckets.test.js index dcb229e22e564..3f8f602e56d17 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js +++ b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.test.js @@ -4,149 +4,163 @@ * you may not use this file except in compliance with the Elastic License. */ -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; import moment from 'moment'; -import { TimeBuckets, getBoundsRoundedToInterval, calcEsInterval } from '../time_buckets'; +import { TimeBuckets, getBoundsRoundedToInterval, calcEsInterval } from './time_buckets'; + +jest.mock( + './dependency_cache', + () => ({ + getUiSettings: () => { + return { + get(val) { + switch (val) { + case 'histogram:barTarget': + return 50; + case 'histogram:maxBars': + return 100; + } + }, + }; + }, + }), + { virtual: true } +); describe('ML - time buckets', () => { let autoBuckets; let customBuckets; beforeEach(() => { - ngMock.module('kibana'); - ngMock.inject(() => { - autoBuckets = new TimeBuckets(); - autoBuckets.setInterval('auto'); + autoBuckets = new TimeBuckets(); + autoBuckets.setInterval('auto'); - customBuckets = new TimeBuckets(); - customBuckets.setInterval('auto'); - customBuckets.setBarTarget(500); - customBuckets.setMaxBars(550); - }); + customBuckets = new TimeBuckets(); + customBuckets.setInterval('auto'); + customBuckets.setBarTarget(500); + customBuckets.setMaxBars(550); }); describe('default bar target', () => { - it('returns correct interval for default target with hour bounds', () => { + test('returns correct interval for default target with hour bounds', () => { const hourBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-01T01:00:00.000'), }; autoBuckets.setBounds(hourBounds); const hourResult = autoBuckets.getInterval(); - expect(hourResult.asSeconds()).to.be(60); // 1 minute + expect(hourResult.asSeconds()).toBe(60); // 1 minute }); - it('returns correct interval for default target with day bounds', () => { + test('returns correct interval for default target with day bounds', () => { const dayBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-02T00:00:00.000'), }; autoBuckets.setBounds(dayBounds); const dayResult = autoBuckets.getInterval(); - expect(dayResult.asSeconds()).to.be(1800); // 30 minutes + expect(dayResult.asSeconds()).toBe(1800); // 30 minutes }); - it('returns correct interval for default target with week bounds', () => { + test('returns correct interval for default target with week bounds', () => { const weekBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-08T00:00:00.000'), }; autoBuckets.setBounds(weekBounds); const weekResult = autoBuckets.getInterval(); - expect(weekResult.asSeconds()).to.be(14400); // 4 hours + expect(weekResult.asSeconds()).toBe(14400); // 4 hours }); - it('returns correct interval for default target with 30 day bounds', () => { + test('returns correct interval for default target with 30 day bounds', () => { const monthBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-31T00:00:00.000'), }; autoBuckets.setBounds(monthBounds); const monthResult = autoBuckets.getInterval(); - expect(monthResult.asSeconds()).to.be(86400); // 1 day + expect(monthResult.asSeconds()).toBe(86400); // 1 day }); - it('returns correct interval for default target with year bounds', () => { + test('returns correct interval for default target with year bounds', () => { const yearBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2018-01-01T00:00:00.000'), }; autoBuckets.setBounds(yearBounds); const yearResult = autoBuckets.getInterval(); - expect(yearResult.asSeconds()).to.be(604800); // 1 week + expect(yearResult.asSeconds()).toBe(604800); // 1 week }); - it('returns correct interval as multiple of 3 hours for default target with 2 week bounds', () => { + test('returns correct interval as multiple of 3 hours for default target with 2 week bounds', () => { const weekBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-15T00:00:00.000'), }; autoBuckets.setBounds(weekBounds); const weekResult = autoBuckets.getIntervalToNearestMultiple(10800); // 3 hours - expect(weekResult.asSeconds()).to.be(32400); // 9 hours + expect(weekResult.asSeconds()).toBe(32400); // 9 hours }); }); describe('custom bar target', () => { - it('returns correct interval for 500 bar target with hour bounds', () => { + test('returns correct interval for 500 bar target with hour bounds', () => { const hourBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-01T01:00:00.000'), }; customBuckets.setBounds(hourBounds); const hourResult = customBuckets.getInterval(); - expect(hourResult.asSeconds()).to.be(10); // 10 seconds + expect(hourResult.asSeconds()).toBe(10); // 10 seconds }); - it('returns correct interval for 500 bar target with day bounds', () => { + test('returns correct interval for 500 bar target with day bounds', () => { const dayBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-02T00:00:00.000'), }; customBuckets.setBounds(dayBounds); const dayResult = customBuckets.getInterval(); - expect(dayResult.asSeconds()).to.be(300); // 5 minutes + expect(dayResult.asSeconds()).toBe(300); // 5 minutes }); - it('returns correct interval for 500 bar target with week bounds', () => { + test('returns correct interval for 500 bar target with week bounds', () => { const weekBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-08T00:00:00.000'), }; customBuckets.setBounds(weekBounds); const weekResult = customBuckets.getInterval(); - expect(weekResult.asSeconds()).to.be(1800); // 30 minutes + expect(weekResult.asSeconds()).toBe(1800); // 30 minutes }); - it('returns correct interval for 500 bar target with 30 day bounds', () => { + test('returns correct interval for 500 bar target with 30 day bounds', () => { const monthBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-01-31T00:00:00.000'), }; customBuckets.setBounds(monthBounds); const monthResult = customBuckets.getInterval(); - expect(monthResult.asSeconds()).to.be(7200); // 2 hours + expect(monthResult.asSeconds()).toBe(7200); // 2 hours }); - it('returns correct interval for 500 bar target with year bounds', () => { + test('returns correct interval for 500 bar target with year bounds', () => { const yearBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2018-01-01T00:00:00.000'), }; customBuckets.setBounds(yearBounds); const yearResult = customBuckets.getInterval(); - expect(yearResult.asSeconds()).to.be(86400); // 1 day + expect(yearResult.asSeconds()).toBe(86400); // 1 day }); - it('returns correct interval as multiple of 3 hours for 500 bar target with 90 day bounds', () => { + test('returns correct interval as multiple of 3 hours for 500 bar target with 90 day bounds', () => { const weekBounds = { min: moment('2017-01-01T00:00:00.000'), max: moment('2017-04-01T00:00:00.000'), }; customBuckets.setBounds(weekBounds); const weekResult = customBuckets.getIntervalToNearestMultiple(10800); // 3 hours - expect(weekResult.asSeconds()).to.be(21600); // 6 hours + expect(weekResult.asSeconds()).toBe(21600); // 6 hours }); }); @@ -158,104 +172,104 @@ describe('ML - time buckets', () => { max: moment('2017-10-26T09:08:07.000+00:00'), }; - it('returns correct bounds for 4h interval without inclusive end', () => { + test('returns correct bounds for 4h interval without inclusive end', () => { const bounds4h = getBoundsRoundedToInterval(testBounds, moment.duration(4, 'hours'), false); - expect(bounds4h.min.valueOf()).to.be(moment('2017-01-05T08:00:00.000+00:00').valueOf()); - expect(bounds4h.max.valueOf()).to.be(moment('2017-10-26T11:59:59.999+00:00').valueOf()); + expect(bounds4h.min.valueOf()).toBe(moment('2017-01-05T08:00:00.000+00:00').valueOf()); + expect(bounds4h.max.valueOf()).toBe(moment('2017-10-26T11:59:59.999+00:00').valueOf()); }); - it('returns correct bounds for 4h interval with inclusive end', () => { + test('returns correct bounds for 4h interval with inclusive end', () => { const bounds4h = getBoundsRoundedToInterval(testBounds, moment.duration(4, 'hours'), true); - expect(bounds4h.min.valueOf()).to.be(moment('2017-01-05T08:00:00.000+00:00').valueOf()); - expect(bounds4h.max.valueOf()).to.be(moment('2017-10-26T12:00:00.000+00:00').valueOf()); + expect(bounds4h.min.valueOf()).toBe(moment('2017-01-05T08:00:00.000+00:00').valueOf()); + expect(bounds4h.max.valueOf()).toBe(moment('2017-10-26T12:00:00.000+00:00').valueOf()); }); - it('returns correct bounds for 1d interval without inclusive end', () => { + test('returns correct bounds for 1d interval without inclusive end', () => { const bounds4h = getBoundsRoundedToInterval(testBounds, moment.duration(1, 'days'), false); - expect(bounds4h.min.valueOf()).to.be(moment('2017-01-05T00:00:00.000+00:00').valueOf()); - expect(bounds4h.max.valueOf()).to.be(moment('2017-10-26T23:59:59.999+00:00').valueOf()); + expect(bounds4h.min.valueOf()).toBe(moment('2017-01-05T00:00:00.000+00:00').valueOf()); + expect(bounds4h.max.valueOf()).toBe(moment('2017-10-26T23:59:59.999+00:00').valueOf()); }); - it('returns correct bounds for 1d interval with inclusive end', () => { + test('returns correct bounds for 1d interval with inclusive end', () => { const bounds4h = getBoundsRoundedToInterval(testBounds, moment.duration(1, 'days'), true); - expect(bounds4h.min.valueOf()).to.be(moment('2017-01-05T00:00:00.000+00:00').valueOf()); - expect(bounds4h.max.valueOf()).to.be(moment('2017-10-27T00:00:00.000+00:00').valueOf()); + expect(bounds4h.min.valueOf()).toBe(moment('2017-01-05T00:00:00.000+00:00').valueOf()); + expect(bounds4h.max.valueOf()).toBe(moment('2017-10-27T00:00:00.000+00:00').valueOf()); }); }); describe('calcEsInterval', () => { - it('returns correct interval for various durations', () => { - expect(calcEsInterval(moment.duration(500, 'ms'))).to.eql({ + test('returns correct interval for various durations', () => { + expect(calcEsInterval(moment.duration(500, 'ms'))).toEqual({ value: 500, unit: 'ms', expression: '500ms', }); - expect(calcEsInterval(moment.duration(1000, 'ms'))).to.eql({ + expect(calcEsInterval(moment.duration(1000, 'ms'))).toEqual({ value: 1, unit: 's', expression: '1s', }); - expect(calcEsInterval(moment.duration(15, 's'))).to.eql({ + expect(calcEsInterval(moment.duration(15, 's'))).toEqual({ value: 15, unit: 's', expression: '15s', }); - expect(calcEsInterval(moment.duration(60, 's'))).to.eql({ + expect(calcEsInterval(moment.duration(60, 's'))).toEqual({ value: 1, unit: 'm', expression: '1m', }); - expect(calcEsInterval(moment.duration(1, 'm'))).to.eql({ + expect(calcEsInterval(moment.duration(1, 'm'))).toEqual({ value: 1, unit: 'm', expression: '1m', }); - expect(calcEsInterval(moment.duration(60, 'm'))).to.eql({ + expect(calcEsInterval(moment.duration(60, 'm'))).toEqual({ value: 1, unit: 'h', expression: '1h', }); - expect(calcEsInterval(moment.duration(3, 'h'))).to.eql({ + expect(calcEsInterval(moment.duration(3, 'h'))).toEqual({ value: 3, unit: 'h', expression: '3h', }); - expect(calcEsInterval(moment.duration(24, 'h'))).to.eql({ + expect(calcEsInterval(moment.duration(24, 'h'))).toEqual({ value: 1, unit: 'd', expression: '1d', }); - expect(calcEsInterval(moment.duration(3, 'd'))).to.eql({ + expect(calcEsInterval(moment.duration(3, 'd'))).toEqual({ value: 3, unit: 'd', expression: '3d', }); - expect(calcEsInterval(moment.duration(7, 'd'))).to.eql({ + expect(calcEsInterval(moment.duration(7, 'd'))).toEqual({ value: 1, unit: 'w', expression: '1w', }); - expect(calcEsInterval(moment.duration(1, 'w'))).to.eql({ + expect(calcEsInterval(moment.duration(1, 'w'))).toEqual({ value: 1, unit: 'w', expression: '1w', }); - expect(calcEsInterval(moment.duration(4, 'w'))).to.eql({ + expect(calcEsInterval(moment.duration(4, 'w'))).toEqual({ value: 28, unit: 'd', expression: '28d', }); - expect(calcEsInterval(moment.duration(1, 'M'))).to.eql({ + expect(calcEsInterval(moment.duration(1, 'M'))).toEqual({ value: 1, unit: 'M', expression: '1M', }); - expect(calcEsInterval(moment.duration(12, 'M'))).to.eql({ + expect(calcEsInterval(moment.duration(12, 'M'))).toEqual({ value: 1, unit: 'y', expression: '1y', }); - expect(calcEsInterval(moment.duration(1, 'y'))).to.eql({ + expect(calcEsInterval(moment.duration(1, 'y'))).toEqual({ value: 1, unit: 'y', expression: '1y', diff --git a/x-pack/legacy/plugins/ml/public/index.ts b/x-pack/legacy/plugins/ml/public/index.ts index 0057983104cc0..bafeb7277927f 100755 --- a/x-pack/legacy/plugins/ml/public/index.ts +++ b/x-pack/legacy/plugins/ml/public/index.ts @@ -5,8 +5,8 @@ */ import { PluginInitializer } from '../../../../../src/core/public'; -import { MlPlugin, MlPluginSetup, MlPluginStart } from './plugin'; +import { MlPlugin, Setup, Start } from './plugin'; -export const plugin: PluginInitializer = () => new MlPlugin(); +export const plugin: PluginInitializer = () => new MlPlugin(); -export { MlPluginSetup, MlPluginStart }; +export { Setup, Start }; diff --git a/x-pack/legacy/plugins/ml/public/legacy.ts b/x-pack/legacy/plugins/ml/public/legacy.ts index 3e007a18f4c5a..bf431f0986d68 100644 --- a/x-pack/legacy/plugins/ml/public/legacy.ts +++ b/x-pack/legacy/plugins/ml/public/legacy.ts @@ -4,14 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import chrome from 'ui/chrome'; import { npSetup, npStart } from 'ui/new_platform'; -import { PluginInitializerContext } from '../../../../../src/core/public'; +import { PluginInitializerContext } from 'src/core/public'; import { plugin } from '.'; const pluginInstance = plugin({} as PluginInitializerContext); export const setup = pluginInstance.setup(npSetup.core, { - npData: npStart.plugins.data, + data: npStart.plugins.data, + __LEGACY: { + XSRF: chrome.getXsrfToken(), + // @ts-ignore getAppUrl is missing from chrome's definition + APP_URL: chrome.getAppUrl(), + }, }); export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/ml/public/plugin.ts b/x-pack/legacy/plugins/ml/public/plugin.ts index f68d1ffe88216..79af300bce4ec 100644 --- a/x-pack/legacy/plugins/ml/public/plugin.ts +++ b/x-pack/legacy/plugins/ml/public/plugin.ts @@ -4,15 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin as DataPlugin } from 'src/plugins/data/public'; -import { Plugin, CoreStart, CoreSetup } from '../../../../../src/core/public'; +import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; +import { MlDependencies } from './application/app'; -export interface MlSetupDependencies { - npData: ReturnType; -} - -export class MlPlugin implements Plugin { - setup(core: CoreSetup, { npData }: MlSetupDependencies) { +export class MlPlugin implements Plugin { + setup(core: CoreSetup, { data, __LEGACY }: MlDependencies) { core.application.register({ id: 'ml', title: 'Machine learning', @@ -20,9 +16,11 @@ export class MlPlugin implements Plugin { const [coreStart, depsStart] = await core.getStartServices(); const { renderApp } = await import('./application/app'); return renderApp(coreStart, depsStart, { - ...params, - indexPatterns: npData.indexPatterns, - npData, + element: params.element, + appBasePath: params.appBasePath, + onAppLeave: params.onAppLeave, + data, + __LEGACY, }); }, }); @@ -30,11 +28,11 @@ export class MlPlugin implements Plugin { return {}; } - start(core: CoreStart, deps: {}) { + start(core: CoreStart, deps: any) { return {}; } public stop() {} } -export type MlPluginSetup = ReturnType; -export type MlPluginStart = ReturnType; +export type Setup = ReturnType; +export type Start = ReturnType; diff --git a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js b/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js index d6440cae51666..7773d01625aaf 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js +++ b/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js @@ -12,16 +12,11 @@ import { ML_ANNOTATIONS_INDEX_PATTERN, } from '../../../common/constants/index_patterns'; -import { FEATURE_ANNOTATIONS_ENABLED } from '../../../common/constants/feature_flags'; - // Annotations Feature is available if: -// - FEATURE_ANNOTATIONS_ENABLED is set to `true` // - ML_ANNOTATIONS_INDEX_PATTERN index is present // - ML_ANNOTATIONS_INDEX_ALIAS_READ alias is present // - ML_ANNOTATIONS_INDEX_ALIAS_WRITE alias is present export async function isAnnotationsFeatureAvailable(callWithRequest) { - if (!FEATURE_ANNOTATIONS_ENABLED) return false; - try { const indexParams = { index: ML_ANNOTATIONS_INDEX_PATTERN }; diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index 2b9219b2226f5..085f2de06b55e 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -24,7 +24,6 @@ import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; import { checkLicense } from '../lib/check_license'; // @ts-ignore: could not find declaration file for module import { mirrorPluginStatus } from '../../../../server/lib/mirror_plugin_status'; -import { FEATURE_ANNOTATIONS_ENABLED } from '../../common/constants/feature_flags'; import { LICENSE_TYPE } from '../../common/constants/license'; // @ts-ignore: could not find declaration file for module import { annotationRoutes } from '../routes/annotations'; @@ -134,7 +133,7 @@ export class Plugin { public setup(core: MlCoreSetup, plugins: PluginsSetup) { const xpackMainPlugin: MlXpackMainPlugin = plugins.xpackMain; - const { http, injectUiAppVars } = core; + const { http } = core; const pluginId = this.pluginId; mirrorPluginStatus(xpackMainPlugin, plugins.ml); @@ -197,13 +196,6 @@ export class Plugin { ], }; - injectUiAppVars('ml', () => { - return { - kbnIndex: this.config.get('kibana.index'), - mlAnnotationsEnabled: FEATURE_ANNOTATIONS_ENABLED, - }; - }); - // Can access via new platform router's handler function 'context' parameter - context.ml.mlClient const mlClient = core.elasticsearch.createClient('ml', { plugins: [elasticsearchJsPlugin] }); http.registerRouteHandlerContext('ml', (context, request) => {