diff --git a/x-pack/legacy/plugins/ml/public/_hacks.scss b/x-pack/legacy/plugins/ml/public/_hacks.scss index b0a8a43d23096d..39740360d8a840 100644 --- a/x-pack/legacy/plugins/ml/public/_hacks.scss +++ b/x-pack/legacy/plugins/ml/public/_hacks.scss @@ -1,21 +1,10 @@ .tab-datavisualizer_index_select, .tab-timeseriesexplorer, -.tab-explorer, -.tab-jobs { +.tab-explorer, { // Make all page background white until More of the pages use EuiPage to wrap in panel-like components background-color: $euiColorEmptyShade; } -.tab-jobs { - label { - display: inline-block; - } - - .validation-error { - margin-top: $euiSizeXS; - } -} - // ML specific bootstrap hacks .button-wrapper { display: inline; diff --git a/x-pack/legacy/plugins/ml/public/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/breadcrumbs.ts similarity index 76% rename from x-pack/legacy/plugins/ml/public/breadcrumbs.js rename to x-pack/legacy/plugins/ml/public/breadcrumbs.ts index bdde734be7c1a3..ba4703d4818ff5 100644 --- a/x-pack/legacy/plugins/ml/public/breadcrumbs.js +++ b/x-pack/legacy/plugins/ml/public/breadcrumbs.ts @@ -8,28 +8,28 @@ import { i18n } from '@kbn/i18n'; export const ML_BREADCRUMB = Object.freeze({ text: i18n.translate('xpack.ml.machineLearningBreadcrumbLabel', { - defaultMessage: 'Machine Learning' + defaultMessage: 'Machine Learning', }), - href: '#/' + href: '#/', }); export const SETTINGS = Object.freeze({ text: i18n.translate('xpack.ml.settingsBreadcrumbLabel', { - defaultMessage: 'Settings' + defaultMessage: 'Settings', }), - href: '#/settings?' + href: '#/settings?', }); export const ANOMALY_DETECTION_BREADCRUMB = Object.freeze({ text: i18n.translate('xpack.ml.anomalyDetectionBreadcrumbLabel', { - defaultMessage: 'Anomaly Detection' + defaultMessage: 'Anomaly Detection', }), - href: '#/jobs?' + href: '#/jobs?', }); export const DATA_VISUALIZER_BREADCRUMB = Object.freeze({ text: i18n.translate('xpack.ml.datavisualizerBreadcrumbLabel', { - defaultMessage: 'Data Visualizer' + defaultMessage: 'Data Visualizer', }), - href: '#/datavisualizer?' + href: '#/datavisualizer?', }); diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx index 6549df35ba381c..07a924caae7724 100644 --- a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx +++ b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx @@ -4,25 +4,77 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { FC, ReactElement } from 'react'; -import { EuiCard, EuiIcon, IconType } from '@elastic/eui'; +import { + EuiIcon, + IconType, + EuiText, + EuiTitle, + EuiFlexItem, + EuiFlexGroup, + EuiPanel, + EuiLink, +} from '@elastic/eui'; interface Props { - iconType: IconType; - title: string; - description: string; - onClick(): void; + icon: IconType | ReactElement; + iconAreaLabel?: string; + title: any; + description: any; + href?: string; + onClick?: () => void; + isDisabled?: boolean; + 'data-test-subj'?: string; } // Component for rendering a card which links to the Create Job page, displaying an // icon, card title, description and link. -export const CreateJobLinkCard: FC = ({ iconType, title, description, onClick }) => ( - } - title={title} - description={description} - onClick={onClick} - /> -); +export const CreateJobLinkCard: FC = ({ + icon, + iconAreaLabel, + title, + description, + onClick, + href, + isDisabled, + 'data-test-subj': dateTestSubj, +}) => { + const linkHrefAndOnClickProps = { + ...(href ? { href } : {}), + ...(onClick ? { onClick } : {}), + }; + return ( + + + + + {typeof icon === 'string' ? ( + + ) : ( + icon + )} + + + +

{title}

+
+ +

{description}

+
+
+
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/_data_recognizer.scss b/x-pack/legacy/plugins/ml/public/components/data_recognizer/_data_recognizer.scss deleted file mode 100644 index b915be2ab84536..00000000000000 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/_data_recognizer.scss +++ /dev/null @@ -1,31 +0,0 @@ -ml-data-recognizer { - .ml-data-recognizer-logo { - width: $euiSizeXL; - } -} - -// Moved here from /home since it's no longer being used there -.synopsis { - display: flex; - flex-grow: 1; - cursor: pointer; - - &:hover, - &:focus { - text-decoration: none; - - .synopsisTitle { - text-decoration: underline; - } - } -} - -.synopsisTitle { - font-size: $euiSize; - font-weight: normal; - color: $euiColorPrimary; -} - -.synopsisIcon { - padding-top: $euiSizeS; -} diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/_index.scss b/x-pack/legacy/plugins/ml/public/components/data_recognizer/_index.scss deleted file mode 100644 index 67cc4372ea6225..00000000000000 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'data_recognizer'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts b/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts index c8a7bba2d189f5..e7d191a31e034e 100644 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts +++ b/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts @@ -7,9 +7,14 @@ import { FC } from 'react'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; declare const DataRecognizer: FC<{ indexPattern: IndexPattern; - results: any; - className: string; + savedSearch?: SavedSearch; + results: { + count: number; + onChange?: Function; + }; + className?: string; }>; diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js b/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js index fd754ee5191046..b303ed9b7f008b 100644 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js +++ b/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js @@ -57,9 +57,9 @@ export class DataRecognizer extends Component { render() { return ( -
+ <> {this.state.results} -
+ ); } } diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js b/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js index 60dc38f2291f89..6f511abf89e310 100644 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js +++ b/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js @@ -11,7 +11,9 @@ import PropTypes from 'prop-types'; import { EuiIcon, + EuiFlexItem } from '@elastic/eui'; +import { CreateJobLinkCard } from '../create_job_link_card'; export const RecognizedResult = ({ config, @@ -28,35 +30,23 @@ export const RecognizedResult = ({ // if a logo is available, use that, otherwise display the id // the logo should be a base64 encoded image or an eui icon if(config.logo && config.logo.icon) { - logo =
; + logo = ; } else if (config.logo && config.logo.src) { - logo =
; + logo = ; } else { logo =

{config.id}

; } return ( -
- -
-
-
- {logo} -
-
-

{config.title}

-
-

- - {config.description} - -

-
-
-
-
-
-
+ + + ); }; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index 9a291cabf558f9..c8295a1e3d8db3 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern } from 'ui/index_patterns'; -import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; import { useUiChromeContext } from '../../../../contexts/ui/use_ui_chrome_context'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; @@ -63,11 +63,9 @@ export const ActionsPanel: FC = ({ indexPattern }) => {

- + + + @@ -80,7 +78,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { = ({ indexPattern }) => { 'Use the full range of options to create a job for more advanced use cases', })} onClick={openAdvancedJobWizard} + href={`${basePath}/app/ml#/jobs/new_job/advanced?index=${indexPattern}`} /> ); diff --git a/x-pack/legacy/plugins/ml/public/index.scss b/x-pack/legacy/plugins/ml/public/index.scss index 4cab633d5fa569..a3fefb7b1fac86 100644 --- a/x-pack/legacy/plugins/ml/public/index.scss +++ b/x-pack/legacy/plugins/ml/public/index.scss @@ -36,7 +36,6 @@ @import 'components/chart_tooltip/index'; @import 'components/confirm_modal/index'; @import 'components/controls/index'; - @import 'components/data_recognizer/index'; @import 'components/documentation_help_link/index'; @import 'components/entity_cell/index'; @import 'components/field_title_bar/index'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts similarity index 55% rename from x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.js rename to x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts index d066a524d70aab..35e9c3326a4ccf 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.js +++ b/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ - -import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../breadcrumbs'; import { i18n } from '@kbn/i18n'; +import { Breadcrumb } from 'ui/chrome'; +import { + ANOMALY_DETECTION_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + ML_BREADCRUMB, +} from '../breadcrumbs'; - -export function getJobManagementBreadcrumbs() { +export function getJobManagementBreadcrumbs(): Breadcrumb[] { // Whilst top level nav menu with tabs remains, // use root ML breadcrumb. return [ @@ -17,93 +20,93 @@ export function getJobManagementBreadcrumbs() { ANOMALY_DETECTION_BREADCRUMB, { text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { - defaultMessage: 'Job Management' + defaultMessage: 'Job Management', }), - href: '' - } + href: '', + }, ]; } -export function getCreateJobBreadcrumbs() { +export function getCreateJobBreadcrumbs(): Breadcrumb[] { return [ ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, { text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', { - defaultMessage: 'Create job' + defaultMessage: 'Create job', }), - href: '#/jobs/new_job' - } + href: '#/jobs/new_job', + }, ]; } -export function getCreateSingleMetricJobBreadcrumbs() { +export function getCreateSingleMetricJobBreadcrumbs(): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { - defaultMessage: 'Single metric' + defaultMessage: 'Single metric', }), - href: '' - } + href: '', + }, ]; } -export function getCreateMultiMetricJobBreadcrumbs() { +export function getCreateMultiMetricJobBreadcrumbs(): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { - defaultMessage: 'Multi metric' + defaultMessage: 'Multi metric', }), - href: '' - } + href: '', + }, ]; } -export function getCreatePopulationJobBreadcrumbs() { +export function getCreatePopulationJobBreadcrumbs(): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { - defaultMessage: 'Population' + defaultMessage: 'Population', }), - href: '' - } + href: '', + }, ]; } -export function getAdvancedJobConfigurationBreadcrumbs() { +export function getAdvancedJobConfigurationBreadcrumbs(): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { - defaultMessage: 'Advanced configuration' + defaultMessage: 'Advanced configuration', }), - href: '' - } + href: '', + }, ]; } -export function getCreateRecognizerJobBreadcrumbs($routeParams) { +export function getCreateRecognizerJobBreadcrumbs($routeParams: any): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: $routeParams.id, - href: '' - } + href: '', + }, ]; } -export function getDataVisualizerIndexOrSearchBreadcrumbs() { +export function getDataVisualizerIndexOrSearchBreadcrumbs(): Breadcrumb[] { return [ ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB, { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { - defaultMessage: 'Select index or search' + defaultMessage: 'Select index or search', }), - href: '' - } + href: '', + }, ]; } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/_wizard.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/_wizard.scss index 7eadb9a8ce77a0..def24f6d6a7476 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/_wizard.scss +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/_wizard.scss @@ -1,56 +1,3 @@ -.job-type-gallery { - width: 100%; - padding-right: $euiSizeS; - padding-left: $euiSizeS; - background-color: $euiColorLightestShade; - flex: 1 0 auto; - - .job-types-content { - max-width: 1200px; // SASSTODO: Proper calc - margin-right: auto; - margin-left: auto; - } - - .synopsis { - display: flex; - flex-grow: 1; - - .synopsisTitle { - font-size: $euiFontSize; - font-weight: normal; - color: $euiColorPrimary; - } - - .synopsisIcon { - padding-top: $euiSizeS; - } - } - - .synopsis:hover { - text-decoration: none; - - .synopsisTitle { - text-decoration: underline; - } - } - - .euiFlexItem.disabled { - cursor: not-allowed; - } - - .synopsis.disabled { - pointer-events: none; - - .synopsisTitle { - color: $euiColorDarkShade; - } - } - - .index-warning { - border: $euiBorderThin; - } -} - .index-or-saved-search-selection { .kuiBarSection .kuiButtonGroup { display: none; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/index.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/index.js index c2fb4646306692..61ce488f69014b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/index.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/index.js @@ -9,5 +9,4 @@ // SASS TODO: Import wizard.scss instead // import 'plugins/kibana/visualize/wizard/wizard.less'; import './steps/index_or_search'; -import './steps/job_type'; import 'plugins/ml/components/data_recognizer'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/__tests__/job_type_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/__tests__/job_type_controller.js deleted file mode 100644 index f8fd13b2ae36e1..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/__tests__/job_type_controller.js +++ /dev/null @@ -1,46 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from 'plugins/ml/util/index_utils'; - -describe('ML - Job Type Controller', () => { - beforeEach(() => { - ngMock.module('kibana'); - }); - - it('Initialize Job Type Controller', (done) => { - const stub = sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function ($rootScope, $controller, $route) { - // Set up the $route current props required for the tests. - $route.current = { - locals: { - indexPattern: { - id: 'test_id', - title: 'test_pattern' - }, - savedSearch: {} - } - }; - - const scope = $rootScope.$new(); - - expect(() => { - $controller('MlNewJobStepJobType', { $scope: scope }); - }).to.not.throwError(); - - expect(scope.indexWarningTitle).to.eql('Index pattern test_pattern is not time based'); - stub.restore(); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/index.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/index.js deleted file mode 100644 index 9a9fa7e73b9f1e..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/index.js +++ /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. - */ - - - -import './job_type_controller'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html deleted file mode 100644 index 1dc3aea215d93a..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html +++ /dev/null @@ -1,274 +0,0 @@ - - - -
- -
-

-
- -
-
-
-
- - - - - - - {{indexWarningTitle}} -
-
-

- -
- -

-
-
-
-
-
- -
-
-

-

-

-
-
- -
-
- -
-

-

-
- -
- - - -
- -
-

-

-
-
- - - -
- -
-
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js deleted file mode 100644 index df7768ee9f0c84..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js +++ /dev/null @@ -1,103 +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. - */ - - - -/* - * Controller for the second step in the Create Job wizard, allowing - * the user to select the type of job they wish to create. - */ - -import uiRoutes from 'ui/routes'; -import { i18n } from '@kbn/i18n'; -import { checkLicenseExpired } from 'plugins/ml/license/check_license'; -import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { getCreateJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; -import { SearchItemsProvider } from 'plugins/ml/jobs/new_job/utils/new_job_utils'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch, timeBasedIndexCheck } from 'plugins/ml/util/index_utils'; -import { addItemToRecentlyAccessed } from 'plugins/ml/util/recently_accessed'; -import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; -import template from './job_type.html'; -import { timefilter } from 'ui/timefilter'; - -uiRoutes - .when('/jobs/new_job/step/job_type', { - template, - k7Breadcrumbs: getCreateJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - } - }); - - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module.controller('MlNewJobStepJobType', - function ($scope, Private) { - - timefilter.disableTimeRangeSelector(); // remove time picker from top of page - timefilter.disableAutoRefreshSelector(); // remove time picker from top of page - - const createSearchItems = Private(SearchItemsProvider); - const { - indexPattern, - savedSearch } = createSearchItems(); - - // check to see that the index pattern is time based. - // if it isn't, display a warning and disable all links - $scope.indexWarningTitle = ''; - $scope.isTimeBasedIndex = timeBasedIndexCheck(indexPattern); - if ($scope.isTimeBasedIndex === false) { - $scope.indexWarningTitle = (savedSearch.id === undefined) ? - i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { - defaultMessage: 'Index pattern {indexPatternTitle} is not time based', - values: { indexPatternTitle: indexPattern.title } - }) - : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternFromSavedSearchNotTimeBasedMessage', { - defaultMessage: '{savedSearchTitle} uses index pattern {indexPatternTitle} which is not time based', - values: { - savedSearchTitle: savedSearch.title, - indexPatternTitle: indexPattern.title - } - }); - } - - $scope.indexPattern = indexPattern; - $scope.savedSearch = savedSearch; - $scope.recognizerResults = { - count: 0, - onChange() { - $scope.$applyAsync(); - } - }; - - $scope.pageTitleLabel = (savedSearch.id !== undefined) ? - i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { - defaultMessage: 'saved search {savedSearchTitle}', - values: { savedSearchTitle: savedSearch.title } - }) - : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { - defaultMessage: 'index pattern {indexPatternTitle}', - values: { indexPatternTitle: indexPattern.title } - }); - - $scope.getUrl = function (basePath) { - return (savedSearch.id === undefined) ? `${basePath}?index=${indexPattern.id}` : - `${basePath}?savedSearchId=${savedSearch.id}`; - }; - - $scope.addSelectionToRecentlyAccessed = function () { - const title = (savedSearch.id === undefined) ? indexPattern.title : savedSearch.title; - const url = $scope.getUrl(''); - addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); - }; - - }); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts index d3feaf087524c8..2366f2c655000d 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts @@ -6,3 +6,5 @@ import './pages/new_job/route'; import './pages/new_job/directive'; +import './pages/job_type/route'; +import './pages/job_type/directive'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/__test__/directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/__test__/directive.js new file mode 100644 index 00000000000000..5be526f2eb2c02 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/__test__/directive.js @@ -0,0 +1,45 @@ +/* + * 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 ngMock from 'ng_mock'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; + +// Import this way to be able to stub/mock functions later on in the tests using sinon. +import * as indexUtils from 'plugins/ml/util/index_utils'; + +describe('ML - Job Type Directive', () => { + let $scope; + let $compile; + let $element; + + beforeEach(ngMock.module('kibana')); + beforeEach(() => { + ngMock.inject(function ($injector) { + $compile = $injector.get('$compile'); + const $rootScope = $injector.get('$rootScope'); + $scope = $rootScope.$new(); + }); + }); + + afterEach(() => { + $scope.$destroy(); + }); + + it('Initialize Job Type Directive', done => { + sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); + ngMock.inject(function () { + expect(() => { + $element = $compile('')($scope); + }).to.not.throwError(); + + // directive has scope: false + const scope = $element.isolateScope(); + expect(scope).to.eql(undefined); + done(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx new file mode 100644 index 00000000000000..4ad689a943160c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx @@ -0,0 +1,65 @@ +/* + * 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 ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); +import { timefilter } from 'ui/timefilter'; +import { IndexPatterns } from 'ui/index_patterns'; + +import { I18nContext } from 'ui/i18n'; +import { IPrivate } from 'ui/private'; +import { InjectorService } from '../../../../../common/types/angular'; + +import { SearchItemsProvider } from '../../../new_job/utils/new_job_utils'; +import { Page } from './page'; + +import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; + +module.directive('mlJobTypePage', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + // remove time picker from top of page + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + const indexPatterns = $injector.get('indexPatterns'); + const kbnBaseUrl = $injector.get('kbnBaseUrl'); + const kibanaConfig = $injector.get('config'); + const Private = $injector.get('Private'); + + const createSearchItems = Private(SearchItemsProvider); + const { indexPattern, savedSearch, combinedQuery } = createSearchItems(); + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kbnBaseUrl, + kibanaConfig, + }; + + ReactDOM.render( + + + {React.createElement(Page)} + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/page.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/page.tsx new file mode 100644 index 00000000000000..4991039ffa2886 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/page.tsx @@ -0,0 +1,308 @@ +/* + * 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, { FC, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPage, + EuiPageBody, + EuiTitle, + EuiSpacer, + EuiCallOut, + EuiText, + EuiFlexGrid, + EuiFlexItem, + EuiLink, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibanaContext } from '../../../../contexts/kibana'; +import { DataRecognizer } from '../../../../components/data_recognizer'; +import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed'; +import { timeBasedIndexCheck } from '../../../../util/index_utils'; +import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; + +export const Page: FC = () => { + const kibanaContext = useKibanaContext(); + const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); + + const { currentSavedSearch, currentIndexPattern } = kibanaContext; + + const isTimeBasedIndex = timeBasedIndexCheck(currentIndexPattern); + const indexWarningTitle = + !isTimeBasedIndex && currentSavedSearch.id === undefined + ? i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { + defaultMessage: 'Index pattern {indexPatternTitle} is not time based', + values: { indexPatternTitle: currentIndexPattern.title }, + }) + : i18n.translate( + 'xpack.ml.newJob.wizard.jobType.indexPatternFromSavedSearchNotTimeBasedMessage', + { + defaultMessage: + '{savedSearchTitle} uses index pattern {indexPatternTitle} which is not time based', + values: { + savedSearchTitle: currentSavedSearch.title, + indexPatternTitle: currentIndexPattern.title, + }, + } + ); + const pageTitleLabel = + currentSavedSearch.id !== undefined + ? i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { + defaultMessage: 'saved search {savedSearchTitle}', + values: { savedSearchTitle: currentSavedSearch.title }, + }) + : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { + defaultMessage: 'index pattern {indexPatternTitle}', + values: { indexPatternTitle: currentIndexPattern.title }, + }); + + const recognizerResults = { + count: 0, + onChange() { + setRecognizerResultsCount(recognizerResults.count); + }, + }; + + const getUrl = (basePath: string) => { + return currentSavedSearch.id === undefined + ? `${basePath}?index=${currentIndexPattern.id}` + : `${basePath}?savedSearchId=${currentSavedSearch.id}`; + }; + + const addSelectionToRecentlyAccessed = () => { + const title = + currentSavedSearch.id === undefined ? currentIndexPattern.title : currentSavedSearch.title; + const url = getUrl(''); + addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); + + window.location.href = getUrl('#jobs/new_job/datavisualizer'); + }; + + const jobTypes = [ + { + href: getUrl('#jobs/new_job/single_metric'), + icon: { + type: 'createSingleMetricJob', + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricAriaLabel', { + defaultMessage: 'Single metric job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricTitle', { + defaultMessage: 'Single metric', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricDescription', { + defaultMessage: 'Detect anomalies in a single time series.', + }), + id: 'mlJobTypeLinkSingleMetricJob', + }, + { + href: getUrl('#jobs/new_job/multi_metric'), + icon: { + type: 'createMultiMetricJob', + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricAriaLabel', { + defaultMessage: 'Multi metric job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricTitle', { + defaultMessage: 'Multi metric', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricDescription', { + defaultMessage: + 'Detect anomalies in multiple metrics by splitting a time series by a categorical field.', + }), + id: 'mlJobTypeLinkMultiMetricJob', + }, + { + href: getUrl('#jobs/new_job/population'), + icon: { + type: 'createPopulationJob', + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.populationAriaLabel', { + defaultMessage: 'Population job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.populationTitle', { + defaultMessage: 'Population', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.populationDescription', { + defaultMessage: + 'Detect activity that is unusual compared to the behavior of the population.', + }), + id: 'mlJobTypeLinkPopulationJob', + }, + { + href: getUrl('#jobs/new_job/advanced'), + icon: { + type: 'createAdvancedJob', + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedAriaLabel', { + defaultMessage: 'Advanced job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedTitle', { + defaultMessage: 'Advanced', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedDescription', { + defaultMessage: + 'Use the full range of options to create a job for more advanced use cases.', + }), + id: 'mlJobTypeLinkAdvancedJob', + }, + ]; + + return ( + + + +

+ +

+
+ + + {isTimeBasedIndex === false && ( + <> + + +
+ + + +
+ + + )} + + + + + +

+ +

+
+ +

+ +

+
+ + + + + {jobTypes.map(({ href, icon, title, description, id }) => ( + + + + ))} + + + + + + +

+ +

+
+ +

+ +

+
+ + + + + + + } + description={ + + } + onClick={addSelectionToRecentlyAccessed} + href={getUrl('#jobs/new_job/datavisualizer')} + /> + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/route.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/route.ts new file mode 100644 index 00000000000000..b61424998705d6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/route.ts @@ -0,0 +1,27 @@ +/* + * 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 uiRoutes from 'ui/routes'; + +// @ts-ignore +import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; +// @ts-ignore +import { checkLicenseExpired } from '../../../../license/check_license'; +import { checkCreateJobsPrivilege } from '../../../../privilege/check_privilege'; +import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../../util/index_utils'; +import { getCreateJobBreadcrumbs } from '../../../breadcrumbs'; + +uiRoutes.when('/jobs/new_job/step/job_type', { + template: '', + k7Breadcrumbs: getCreateJobBreadcrumbs, + resolve: { + CheckLicense: checkLicenseExpired, + privileges: checkCreateJobsPrivilege, + indexPattern: loadCurrentIndexPattern, + savedSearch: loadCurrentSavedSearch, + checkMlNodesAvailable, + }, +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts index cdca3a810fcdd8..08f05e6884bb35 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts @@ -9,14 +9,12 @@ import uiRoutes from 'ui/routes'; // @ts-ignore import { checkFullLicense } from '../../../../license/check_license'; import { checkGetJobsPrivilege } from '../../../../privilege/check_privilege'; -// @ts-ignore import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../../util/index_utils'; import { getCreateSingleMetricJobBreadcrumbs, getCreateMultiMetricJobBreadcrumbs, getCreatePopulationJobBreadcrumbs, - // @ts-ignore } from '../../../breadcrumbs'; import { Route } from '../../../../../common/types/kibana'; diff --git a/x-pack/legacy/plugins/ml/public/util/index_utils.js b/x-pack/legacy/plugins/ml/public/util/index_utils.js deleted file mode 100644 index dfc6a7735616a6..00000000000000 --- a/x-pack/legacy/plugins/ml/public/util/index_utils.js +++ /dev/null @@ -1,108 +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 { toastNotifications } from 'ui/notify'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { i18n } from '@kbn/i18n'; - -let indexPatternCache = []; -let fullIndexPatterns = []; -let currentIndexPattern = null; -let currentSavedSearch = null; - -export let refreshIndexPatterns = null; - -export function loadIndexPatterns(Private, indexPatterns) { - fullIndexPatterns = indexPatterns; - const savedObjectsClient = Private(SavedObjectsClientProvider); - return savedObjectsClient.find({ - type: 'index-pattern', - fields: ['id', 'title', 'type', 'fields'], - perPage: 10000 - }).then((response) => { - indexPatternCache = response.savedObjects; - - if (refreshIndexPatterns === null) { - refreshIndexPatterns = () => { - return new Promise((resolve, reject) => { - loadIndexPatterns(Private, indexPatterns) - .then((resp) => { - resolve(resp); - }) - .catch((error) => { - reject(error); - }); - }); - }; - } - - return indexPatternCache; - }); -} - -export function getIndexPatterns() { - return indexPatternCache; -} - -export function getIndexPatternNames() { - return indexPatternCache.map(i => (i.attributes && i.attributes.title)); -} - -export function getIndexPatternIdFromName(name) { - for (let j = 0; j < indexPatternCache.length; j++) { - if (indexPatternCache[j].get('title') === name) { - return indexPatternCache[j].id; - } - } - return name; -} - -export function loadCurrentIndexPattern(indexPatterns, $route) { - fullIndexPatterns = indexPatterns; - currentIndexPattern = fullIndexPatterns.get($route.current.params.index); - return currentIndexPattern; -} - -export function getIndexPatternById(id) { - return fullIndexPatterns.get(id); -} - -export function loadCurrentSavedSearch(savedSearches, $route) { - currentSavedSearch = savedSearches.get($route.current.params.savedSearchId); - return currentSavedSearch; -} - -export function getCurrentIndexPattern() { - return currentIndexPattern; -} - -export function getCurrentSavedSearch() { - return currentSavedSearch; -} - -// returns true if the index passed in is time based -// an optional flag will trigger the display a notification at the top of the page -// warning that the index is not time based -export function timeBasedIndexCheck(indexPattern, showNotification = false) { - if (indexPattern.isTimeBased() === false) { - if (showNotification) { - toastNotifications.addWarning({ - title: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationTitle', { - defaultMessage: 'The index pattern {indexPatternTitle} is not based on a time series', - values: { indexPatternTitle: indexPattern.title } - }), - text: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationDescription', { - defaultMessage: 'Anomaly detection only runs over time-based indices' - }), - }); - } - return false; - } else { - return true; - } -} diff --git a/x-pack/legacy/plugins/ml/public/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/util/index_utils.ts new file mode 100644 index 00000000000000..41dd13555726c4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/index_utils.ts @@ -0,0 +1,110 @@ +/* + * 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 { toastNotifications } from 'ui/notify'; +import { i18n } from '@kbn/i18n'; +import { IndexPattern, IndexPatterns } from 'ui/index_patterns'; +import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; +import chrome from 'ui/chrome'; +import { SavedSearchLoader } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; +import { setup as data } from '../../../../../../src/legacy/core_plugins/data/public/legacy'; + +type IndexPatternSavedObject = SimpleSavedObject; + +let indexPatternCache: IndexPatternSavedObject[] = []; +let fullIndexPatterns: IndexPatterns | null = null; + +export let refreshIndexPatterns: (() => Promise) | null = null; + +export function loadIndexPatterns() { + fullIndexPatterns = data.indexPatterns.indexPatterns; + const savedObjectsClient = chrome.getSavedObjectsClient(); + return savedObjectsClient + .find({ + type: 'index-pattern', + fields: ['id', 'title', 'type', 'fields'], + perPage: 10000, + }) + .then(response => { + indexPatternCache = response.savedObjects; + if (refreshIndexPatterns === null) { + refreshIndexPatterns = () => { + return new Promise((resolve, reject) => { + loadIndexPatterns() + .then(resp => { + resolve(resp); + }) + .catch(error => { + reject(error); + }); + }); + }; + } + + return indexPatternCache; + }); +} + +export function getIndexPatterns() { + return indexPatternCache; +} + +export function getIndexPatternNames() { + return indexPatternCache.map(i => i.attributes && i.attributes.title); +} + +export function getIndexPatternIdFromName(name: string) { + for (let j = 0; j < indexPatternCache.length; j++) { + if (indexPatternCache[j].get('title') === name) { + return indexPatternCache[j].id; + } + } + return name; +} + +export function loadCurrentIndexPattern(indexPatterns: IndexPatterns, $route: Record) { + fullIndexPatterns = indexPatterns; + return fullIndexPatterns.get($route.current.params.index); +} + +export function getIndexPatternById(id: string): IndexPattern { + if (fullIndexPatterns !== null) { + return fullIndexPatterns.get(id); + } else { + throw new Error('Index patterns are not initialized!'); + } +} + +export function loadCurrentSavedSearch( + savedSearches: SavedSearchLoader, + $route: Record +) { + return savedSearches.get($route.current.params.savedSearchId); +} + +/** + * Returns true if the index passed in is time based + * an optional flag will trigger the display a notification at the top of the page + * warning that the index is not time based + */ +export function timeBasedIndexCheck(indexPattern: IndexPattern, showNotification = false) { + if (!indexPattern.isTimeBased()) { + if (showNotification) { + toastNotifications.addWarning({ + title: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationTitle', { + defaultMessage: 'The index pattern {indexPatternTitle} is not based on a time series', + values: { indexPatternTitle: indexPattern.title }, + }), + text: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationDescription', { + defaultMessage: 'Anomaly detection only runs over time-based indices', + }), + }); + } + return false; + } else { + return true; + } +} diff --git a/x-pack/legacy/plugins/ml/public/util/recently_accessed.js b/x-pack/legacy/plugins/ml/public/util/recently_accessed.ts similarity index 80% rename from x-pack/legacy/plugins/ml/public/util/recently_accessed.js rename to x-pack/legacy/plugins/ml/public/util/recently_accessed.ts index b642be7d1226a1..9a3d3089dff2bf 100644 --- a/x-pack/legacy/plugins/ml/public/util/recently_accessed.js +++ b/x-pack/legacy/plugins/ml/public/util/recently_accessed.ts @@ -4,38 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ - - // utility functions for managing which links get added to kibana's recently accessed list import { recentlyAccessed } from 'ui/persisted_log'; import { i18n } from '@kbn/i18n'; -export function addItemToRecentlyAccessed(page, itemId, url) { +export function addItemToRecentlyAccessed(page: string, itemId: string, url: string) { let pageLabel = ''; let id = `ml-job-${itemId}`; switch (page) { case 'explorer': pageLabel = i18n.translate('xpack.ml.anomalyExplorerPageLabel', { - defaultMessage: 'Anomaly Explorer' + defaultMessage: 'Anomaly Explorer', }); break; case 'timeseriesexplorer': pageLabel = i18n.translate('xpack.ml.singleMetricViewerPageLabel', { - defaultMessage: 'Single Metric Viewer' + defaultMessage: 'Single Metric Viewer', }); break; case 'jobs/new_job/datavisualizer': pageLabel = i18n.translate('xpack.ml.dataVisualizerPageLabel', { - defaultMessage: 'Data Visualizer' + defaultMessage: 'Data Visualizer', }); id = `ml-datavisualizer-${itemId}`; break; default: + // eslint-disable-next-line no-console console.error('addItemToRecentlyAccessed - No page specified'); return; - break; } url = `ml#/${page}/${url}`;