diff --git a/x-pack/legacy/plugins/ml/common/types/modules.ts b/x-pack/legacy/plugins/ml/common/types/modules.ts new file mode 100644 index 00000000000000..cb012c3641f3bd --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/types/modules.ts @@ -0,0 +1,83 @@ +/* + * 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 { Datafeed, Job } from '../../public/jobs/new_job_new/common/job_creator/configs'; +import { SavedObjectAttributes } from '../../../../../../target/types/core/server'; + +export interface ModuleJob { + id: string; + config: Omit; +} + +export interface KibanaObjectConfig extends SavedObjectAttributes { + description: string; + title: string; + version: number; +} + +export interface KibanaObject { + id: string; + title: string; + config: KibanaObjectConfig; +} + +export interface KibanaObjects { + [objectType: string]: KibanaObject[] | undefined; +} + +/** + * Interface for get_module endpoint response. + */ +export interface Module { + id: string; + title: string; + description: string; + type: string; + logoFile: string; + defaultIndexPattern: string; + query: any; + jobs: ModuleJob[]; + datafeeds: Datafeed[]; + kibana: KibanaObjects; +} + +export interface KibanaObjectResponse { + exists?: boolean; + success?: boolean; + id: string; +} + +export interface SetupError { + body: string; + msg: string; + path: string; + query: {}; + response: string; + statusCode: number; +} + +export interface DatafeedResponse { + id: string; + success: boolean; + started: boolean; + error?: SetupError; +} + +export interface JobResponse { + id: string; + success: boolean; + error?: SetupError; +} + +export interface DataRecognizerConfigResponse { + datafeeds: DatafeedResponse[]; + jobs: JobResponse[]; + kibana: { + search: KibanaObjectResponse; + visualization: KibanaObjectResponse; + dashboard: KibanaObjectResponse; + }; +} diff --git a/x-pack/legacy/plugins/ml/common/util/validators.ts b/x-pack/legacy/plugins/ml/common/util/validators.ts index 746b9ac3de080c..7e0dd624a52e0c 100644 --- a/x-pack/legacy/plugins/ml/common/util/validators.ts +++ b/x-pack/legacy/plugins/ml/common/util/validators.ts @@ -21,3 +21,38 @@ export function maxLengthValidator( } : null; } + +/** + * Provides a validator function for checking against pattern. + * @param pattern + */ +export function patternValidator( + pattern: RegExp +): (value: string) => { pattern: { matchPattern: string } } | null { + return value => + pattern.test(value) + ? null + : { + pattern: { + matchPattern: pattern.toString(), + }, + }; +} + +/** + * Composes multiple validators into a single function + * @param validators + */ +export function composeValidators( + ...validators: Array<(value: string) => { [key: string]: any } | null> +): (value: string) => { [key: string]: any } | null { + return value => { + const validationResult = validators.reduce((acc, validator) => { + return { + ...acc, + ...(validator(value) || {}), + }; + }, {}); + return Object.keys(validationResult).length > 0 ? validationResult : null; + }; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/index.js b/x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts similarity index 81% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/index.js rename to x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts index 8bcbda8d15c481..ffead802bd6f97 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/index.js +++ b/x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './create_job'; +export { usePartialState } from './use_partial_state'; diff --git a/x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts b/x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts new file mode 100644 index 00000000000000..3bb7dbf6578ccc --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts @@ -0,0 +1,21 @@ +/* + * 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 { useState } from 'react'; + +/** + * Custom hook for partial state update. + */ +export function usePartialState(initialValue: T): [T, (update: Partial) => void] { + const [state, setState] = useState(initialValue); + const setFormStateCallback = (update: Partial) => { + setState({ + ...state, + ...update, + }); + }; + return [state, setFormStateCallback]; +} diff --git a/x-pack/legacy/plugins/ml/public/components/json_tooltip/tooltips.js b/x-pack/legacy/plugins/ml/public/components/json_tooltip/tooltips.js index 0728f68bb1ba08..664676d18bb81d 100644 --- a/x-pack/legacy/plugins/ml/public/components/json_tooltip/tooltips.js +++ b/x-pack/legacy/plugins/ml/public/components/json_tooltip/tooltips.js @@ -189,16 +189,6 @@ export const getTooltips = () => { defaultMessage: 'Advanced option. Select to retrieve unfiltered _source document, instead of specified fields.' }) }, - new_job_advanced_settings: { - text: i18n.translate('xpack.ml.tooltips.newJobAdvancedSettingsTooltip', { - defaultMessage: 'Advanced options' - }) - }, - new_job_dedicated_index: { - text: i18n.translate('xpack.ml.tooltips.newJobDedicatedIndexTooltip', { - defaultMessage: 'Select to store results in a separate index for this job.' - }) - }, new_job_enable_model_plot: { text: i18n.translate('xpack.ml.tooltips.newJobEnableModelPlotTooltip', { defaultMessage: 'Select to enable model plot. Stores model information along with results. ' + diff --git a/x-pack/legacy/plugins/ml/public/jobs/index.js b/x-pack/legacy/plugins/ml/public/jobs/index.js index 2baf5c102f4efc..3e60b91aade49b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/index.js +++ b/x-pack/legacy/plugins/ml/public/jobs/index.js @@ -11,6 +11,5 @@ import './new_job/advanced'; import './new_job/simple/single_metric'; import './new_job/simple/multi_metric'; import './new_job/simple/population'; -import './new_job/simple/recognize'; import 'plugins/ml/components/validate_job'; import './new_job_new'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/_index.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/_index.scss index 6cd73f18ec6087..5fb235e7440072 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/_index.scss +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/_index.scss @@ -10,5 +10,4 @@ @import 'multi_metric/index'; // SASSTODO: Needs some rewriting @import 'population/index'; // SASSTODO: Needs some rewriting -@import 'recognize/index'; // SASSTODO: Needs some rewriting @import 'single_metric/index'; // SASSTODO: Needs some rewriting diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss deleted file mode 100644 index e04ae3b3565598..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'create_job/index' \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js deleted file mode 100644 index 2f5302a1411dba..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js +++ /dev/null @@ -1,58 +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 chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import { mlJobService } from '../../../../services/job_service'; -import { ml } from '../../../../services/ml_api_service'; -import { toastNotifications } from 'ui/notify'; - - -// Checks whether the jobs in a data recognizer module have been created. -// Redirects to the Anomaly Explorer to view the jobs if they have been created, -// or the recognizer job wizard for the module if not. -export function checkViewOrCreateJobs(Private, $route, kbnBaseUrl, kbnUrl) { - - return new Promise((resolve, reject) => { - const moduleId = $route.current.params.id; - const indexPatternId = $route.current.params.index; - - // Load the module, and check if the job(s) in the module have been created. - // If so, load the jobs in the Anomaly Explorer. - // Otherwise open the data recognizer wizard for the module. - // Always want to call reject() so as not to load original page. - ml.dataRecognizerModuleJobsExist({ moduleId }) - .then((resp) => { - const basePath = `${chrome.getBasePath()}/app/`; - - if (resp.jobsExist === true) { - const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer'); - window.location.href = `${basePath}${resultsPageUrl}`; - reject(); - } else { - window.location.href = `${basePath}ml#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; - reject(); - } - - }) - .catch((err) => { - console.log(`Error checking whether jobs in module ${moduleId} exists`, err); - toastNotifications.addWarning({ - title: i18n.translate('xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningTitle', { - defaultMessage: 'Error checking module {moduleId}', - values: { moduleId } - }), - text: i18n.translate('xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningDescription', { - defaultMessage: 'An error occurred trying to check whether the jobs in the module have been created.', - }) - }); - - - kbnUrl.redirect(`/jobs`); - reject(); - }); - }); -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/__tests__/create_job_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/__tests__/create_job_controller.js deleted file mode 100644 index 06c6b90323cf0d..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/__tests__/create_job_controller.js +++ /dev/null @@ -1,45 +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'; - - -describe('ML - Recognize Wizard - Create Job Controller', () => { - beforeEach(() => { - ngMock.module('kibana'); - }); - - it('Initialize Create Job Controller', (done) => { - ngMock.inject(function ($rootScope, $controller, $route) { - // Set up the $route current props required for the tests. - $route.current = { - locals: { - indexPattern: {}, - savedSearch: {} - } - }; - - const scope = $rootScope.$new(); - - expect(() => { - $controller('MlCreateRecognizerJobs', { - $route: { - current: { - params: {} - } - }, - $scope: scope - }); - }).to.not.throwError(); - - expect(scope.ui.formValid).to.eql(true); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss deleted file mode 100644 index 0a19ef71ee2150..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss +++ /dev/null @@ -1,193 +0,0 @@ -// SASSTODO: This file needs to be rewritten for proper variable usage and size calcs -.recognizer-job-container { - font-size: $euiFontSizeS; - width: 100%; - margin-right: auto; - margin-left: auto; - padding-left: $euiSize; - padding-right: $euiSize; - - - .job-state-info { - margin-bottom: $euiSize; - } - - .form-controls, .charts-container { - margin: 0px; - margin-right: -$euiSize; - - // SASSTODO: Proper selector - & > div { - border: 1px solid $euiBorderColor; - border-top: 0px; - } - - // SASSTODO: Proper selector - & > h4 { - margin-top: 0px; - margin-bottom: 0px; - padding: 10px; - background-color: $euiColorPrimary; - color: $euiColorEmptyShade; - } - - .btn-load-vis { - border-radius: $euiBorderRadius !important; - margin-top: -2px; - } - } - - // SASSTODO: Proper calc - .advanced-button { - min-width: 23px; - } - - .advanced-button-container { - // SASSTODO: Proper selector - label { - cursor: pointer; - display: inline-block; - } - } - - .advanced-group { - padding: 10px; - background-color: $euiColorLightestShade; - - // SASSTODO: Proper selector - label { - font-weight: $euiFontWeightRegular; - } - } - - .charts-container { - margin-left: -$euiSizeS; - margin-bottom: $euiSizeS; - margin-right: 0px; - line-height: 20px; - - .jobs-list, .save-objects-list { - padding: $euiSizeXS; - - .job-container { - border: 1px solid $euiBorderColor; - // padding: 5px; - margin: $euiSizeXS; - border-radius: $euiBorderRadius; - display: flex; - - .labels { - flex: auto; - margin: $euiBorderRadius; - .title { - color: $euiColorPrimary; - } - .exists { - color: $euiColorMediumShade; - font-style: italic; - - // SASSTODO: Proper selector - span { - font-size: $euiFontSizeXS; - } - } - .sub-title { - color: $euiColorDarkShade; - font-size: $euiFontSizeXS; - margin-top: 2px; - font-style: italic; - } - } - - .results { - flex-grow: 0; - flex-shrink: 0; - border-left: 1px solid $euiBorderColor; - background-color: $euiColorLightestShade; - padding-top: $euiSizeXS; - - opacity: 0; - transition: opacity 0.5s; - - .result-box { - display: inline-block; - vertical-align: middle; - text-align: center; - // SASSTODO: Proper calc - width: 50px; - - .result-box-title { - color: $euiColorMediumShade; - font-size: $euiFontSizeXS; - font-style: italic; - margin-bottom: $euiSizeXS; - } - - .result-box-inner { - width: 20px; - height: 20px; - font-size: $euiFontSizeM; - margin: 0px; - align-items: center; - justify-content: center; - display: inline-flex; - vertical-align: top; - } - } - } - } - } - .jobs-list { - .result-box { - margin-right: $euiSizeS; - } - } - } - - .form-section { - margin: 0px 0px 0px 0px; - border-bottom: 1px solid $euiBorderColor; - padding: $euiSizeS; - overflow: hidden; - - // SASSTODO: Proper selector - & > h4, & > h5 { - margin-top: 0px; - display: inline-block; - } - .form-group:last-child { - margin-bottom: 0px; - } - - // SASSTODO: Proper selector - .help-text { - border: 1px solid $euiBorderColor; - padding: 5px; - background-color: $euiColorEmptyShade; - border-radius: $euiSizeXS; - font-size: $euiFontSizeXS; - } - } - - .form-section:last-of-type { - margin: 0px; - border-bottom: 0px solid $euiBorderColor; - } - - .form-section-collapsed { - height: 46px; - } - - - div.validation-error { - color: $euiColorDanger; - font-size: $euiFontSizeXS; - } -} - - -// hide the default es loading indicator bar as it can't be switched off -// for standard es searches using the http header. -.kbnLoadingIndicator__bar { - display: none; -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss deleted file mode 100644 index ea8b32f6673b1e..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'create_jobs'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html deleted file mode 100644 index 5839bf7c60d04e..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html +++ /dev/null @@ -1,362 +0,0 @@ - - - -
-
-
-

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

-
-
-
-
- -
- -
-
-
-

-
- -
-
-

-
-
- -
-
-
- - {{ ::'xpack.ml.newJob.simple.recognize.jobIdPrefixLabel' | i18n: {defaultMessage: 'Job ID prefix'} }} - - -
{{ ui.validation.checks.jobLabel.message }}
-
-
- - {{ ::'xpack.ml.newJob.simple.recognize.jobGroupsLabel' | i18n: {defaultMessage: 'Job groups'} }} - - -
{{ ui.validation.checks.groupIds.message }}
-
-
-
- - - -
-
- -
-
- -
- -
-
- -
- - - -
- - -
-
-
-

- - -

- -

- - -

- -
- - - - - -
- -
-

- - -

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

-
-
-
-
{{formConfig.jobLabel}}{{job.id}}
-
{{job.jobConfig.description}}
-
{{error}}
-
-
-
-
-
- - - - -
-
-
-
-
- - - - -
-
-
-
-
- - - - -
-
-
-
-
-
- -
-

{{ui.kibanaLabels[key]}}

-
-
-
-
- {{obj.title}} - -
-
{{error}}
-
-
-
-
- - - - - -
-
-
-
-
-
-
-
-
-
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js deleted file mode 100644 index 5365078556e965..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js +++ /dev/null @@ -1,595 +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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import angular from 'angular'; -import 'ui/angular_ui_select'; -import dateMath from '@elastic/datemath'; -import { isJobIdValid, prefixDatafeedId } from 'plugins/ml/../common/util/job_utils'; -import { getCreateRecognizerJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; -import { SearchItemsProvider, addNewJobToRecentlyAccessed } from 'plugins/ml/jobs/new_job/utils/new_job_utils'; - - -import uiRoutes from 'ui/routes'; -import { checkViewOrCreateJobs } from '../check_module'; -import { checkLicenseExpired } from 'plugins/ml/license/check_license'; -import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from 'plugins/ml/util/index_utils'; -import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { CreateRecognizerJobsServiceProvider } from './create_job_service'; -import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import template from './create_job.html'; -import { toastNotifications } from 'ui/notify'; -import { timefilter } from 'ui/timefilter'; - -uiRoutes - .when('/jobs/new_job/recognize', { - template, - k7Breadcrumbs: getCreateRecognizerJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - } - }); - -uiRoutes - .when('/modules/check_view_or_create', { - template, - resolve: { - checkViewOrCreateJobs - } - }); - - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module - .controller('MlCreateRecognizerJobs', function ($scope, $route, Private) { - - const mlCreateRecognizerJobsService = Private(CreateRecognizerJobsServiceProvider); - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - const msgs = mlMessageBarService; - - const SAVE_STATE = { - NOT_SAVED: 0, - SAVING: 1, - SAVED: 2, - FAILED: 3, - PARTIAL_FAILURE: 4 - }; - - const DATAFEED_STATE = { - NOT_STARTED: 0, - STARTING: 1, - STARTED: 2, - FINISHED: 3, - STOPPING: 4, - FAILED: 5 - }; - - $scope.addNewJobToRecentlyAccessed = addNewJobToRecentlyAccessed; - - $scope.SAVE_STATE = SAVE_STATE; - $scope.DATAFEED_STATE = DATAFEED_STATE; - - $scope.overallState = SAVE_STATE.NOT_SAVED; - - const moduleId = $route.current.params.id; - $scope.moduleId = moduleId; - - const createSearchItems = Private(SearchItemsProvider); - const { - indexPattern, - savedSearch, - combinedQuery } = createSearchItems(); - - const pageTitle = (savedSearch.id !== undefined) ? - i18n.translate('xpack.ml.newJob.simple.recognize.savedSearchPageTitle', { - defaultMessage: 'saved search {savedSearchTitle}', - values: { savedSearchTitle: savedSearch.title } - }) : - i18n.translate('xpack.ml.newJob.simple.recognize.indexPatternPageTitle', { - defaultMessage: 'index pattern {indexPatternTitle}', - values: { indexPatternTitle: indexPattern.title } - }); - - $scope.displayQueryWarning = (savedSearch.id !== undefined); - - $scope.hideAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.recognize.hideAdvancedButtonAriaLabel', { - defaultMessage: 'Hide Advanced' - }); - $scope.showAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.recognize.showAdvancedButtonAriaLabel', { - defaultMessage: 'Show Advanced' - }); - $scope.showAdvancedAriaLabel = i18n.translate('xpack.ml.newJob.simple.recognize.showAdvancedAriaLabel', { - defaultMessage: 'Show advanced' - }); - - $scope.ui = { - formValid: true, - indexPattern, - pageTitle, - showJobInput: true, - numberOfJobs: 0, - kibanaLabels: { - dashboard: i18n.translate('xpack.ml.newJob.simple.recognize.dashboardsLabel', { - defaultMessage: 'Dashboards' - }), - search: i18n.translate('xpack.ml.newJob.simple.recognize.searchesLabel', { - defaultMessage: 'Searches' - }), - visualization: i18n.translate('xpack.ml.newJob.simple.recognize.visualizationsLabel', { - defaultMessage: 'Visualizations' - }), - }, - validation: { - checks: { - jobLabel: { valid: true }, - groupIds: { valid: true } - }, - }, - }; - - $scope.formConfig = { - indexPattern, - jobLabel: '', - jobGroups: [], - jobs: [], - kibanaObjects: {}, - start: 0, - end: 0, - useFullIndexData: true, - startDatafeedAfterSave: true, - useDedicatedIndex: false, - }; - - $scope.resultsUrl = ''; - - $scope.resetJob = function () { - $scope.overallState = SAVE_STATE.NOT_SAVED; - $scope.formConfig.jobs = []; - $scope.formConfig.kibanaObjects = {}; - - loadJobConfigs(); - }; - - function loadJobConfigs() { - // load the job and datafeed configs as well as the kibana saved objects - // from the recognizer endpoint - ml.getDataRecognizerModule({ moduleId }) - .then(resp => { - // populate the jobs and datafeeds - if (resp.jobs && resp.jobs.length) { - - const tempGroups = {}; - - resp.jobs.forEach((job) => { - $scope.formConfig.jobs.push({ - id: job.id, - jobConfig: job.config, - jobState: SAVE_STATE.NOT_SAVED, - datafeedId: null, - datafeedConfig: {}, - datafeedState: SAVE_STATE.NOT_SAVED, - runningState: DATAFEED_STATE.NOT_STARTED, - errors: [] - }); - $scope.ui.numberOfJobs++; - - // read the groups list from each job and create a deduplicated jobGroups list - if (job.config.groups && job.config.groups.length) { - job.config.groups.forEach((group) => { - tempGroups[group] = null; - }); - } - }); - $scope.formConfig.jobGroups = Object.keys(tempGroups); - - resp.datafeeds.forEach((datafeed) => { - const job = _.find($scope.formConfig.jobs, { id: datafeed.config.job_id }); - if (job !== undefined) { - const datafeedId = mlJobService.getDatafeedId(job.id); - job.datafeedId = datafeedId; - job.datafeedConfig = datafeed.config; - } - }); - } - // populate the kibana saved objects - if (resp.kibana) { - _.each(resp.kibana, (obj, key) => { - $scope.formConfig.kibanaObjects[key] = obj.map((o) => { - return { - id: o.id, - title: o.title, - saveState: SAVE_STATE.NOT_SAVED, - config: o.config, - exists: false, - errors: [], - }; - }); - }); - // check to see if any of the saved objects already exist. - // if they do, they are marked as such and greyed out. - checkIfKibanaObjectsExist($scope.formConfig.kibanaObjects); - } - $scope.$applyAsync(); - }); - } - - // toggle kibana's timepicker - $scope.changeUseFullIndexData = function () { - const shouldEnableTimeFilter = !$scope.formConfig.useFullIndexData; - if (shouldEnableTimeFilter) { - timefilter.enableTimeRangeSelector(); - } else { - timefilter.disableTimeRangeSelector(); - } - $scope.$applyAsync(); - }; - - $scope.changeJobLabelCase = function () { - $scope.formConfig.jobLabel = $scope.formConfig.jobLabel.toLowerCase(); - }; - - $scope.save = function () { - if (validateJobs()) { - msgs.clear(); - $scope.overallState = SAVE_STATE.SAVING; - angular.element('.results').css('opacity', 1); - // wait 500ms for the results section to fade in. - window.setTimeout(() => { - // save jobs,datafeeds and kibana savedObjects - saveDataRecognizerItems() - .then(() => { - // open jobs and save start datafeeds - if ($scope.formConfig.startDatafeedAfterSave) { - startDatafeeds() - .then(() => { - // everything saved correctly and datafeeds have started. - $scope.setOverallState(); - }).catch(() => { - $scope.setOverallState(); - }); - } else { - // datafeeds didn't need to be started so finish - $scope.setOverallState(); - } - }); - }, 500); - } - }; - - // call the the setupModuleConfigs endpoint to create the jobs, datafeeds and saved objects - function saveDataRecognizerItems() { - return new Promise((resolve) => { - // set all jobs, datafeeds and saved objects to a SAVING state - // i.e. display spinners - setAllToSaving(); - - const prefix = $scope.formConfig.jobLabel; - const indexPatternName = $scope.formConfig.indexPattern.title; - const groups = $scope.formConfig.jobGroups; - const useDedicatedIndex = $scope.formConfig.useDedicatedIndex; - const tempQuery = (savedSearch.id === undefined) ? - undefined : combinedQuery; - - ml.setupDataRecognizerConfig({ moduleId, prefix, groups, query: tempQuery, indexPatternName, useDedicatedIndex }) - .then((resp) => { - if (resp.jobs) { - $scope.formConfig.jobs.forEach((job) => { - // check results from saving the jobs - const jobId = `${prefix}${job.id}`; - const jobResult = resp.jobs.find(j => j.id === jobId); - if (jobResult !== undefined) { - if (jobResult.success) { - job.jobState = SAVE_STATE.SAVED; - } else { - job.jobState = SAVE_STATE.FAILED; - if (jobResult.error && jobResult.error.msg) { - job.errors.push(jobResult.error.msg); - } - } - } else { - job.jobState = SAVE_STATE.FAILED; - job.errors.push( - i18n.translate('xpack.ml.newJob.simple.recognize.job.couldNotSaveJobErrorMessage', { - defaultMessage: 'Could not save job {jobId}', - values: { jobId } - }) - ); - } - - // check results from saving the datafeeds - const datafeedId = prefixDatafeedId(job.datafeedId, prefix); - const datafeedResult = resp.datafeeds.find(d => d.id === datafeedId); - if (datafeedResult !== undefined) { - if (datafeedResult.success) { - job.datafeedState = SAVE_STATE.SAVED; - } else { - job.datafeedState = SAVE_STATE.FAILED; - if (datafeedResult.error && datafeedResult.error.msg) { - job.errors.push(datafeedResult.error.msg); - } - } - } else { - job.datafeedState = SAVE_STATE.FAILED; - job.errors.push( - i18n.translate('xpack.ml.newJob.simple.recognize.datafeed.couldNotSaveDatafeedErrorMessage', { - defaultMessage: 'Could not save datafeed {datafeedId}', - values: { datafeedId } - }) - ); - } - $scope.$applyAsync(); - }); - } - - if (resp.kibana) { - _.each($scope.formConfig.kibanaObjects, (kibanaObject, objName) => { - kibanaObject.forEach((obj) => { - // check the results from saving the saved objects - const kibanaObjectResult = resp.kibana[objName].find(o => o.id === obj.id); - if (kibanaObjectResult !== undefined) { - if (kibanaObjectResult.success || kibanaObjectResult.success === false && kibanaObjectResult.exists === true) { - obj.saveState = SAVE_STATE.SAVED; - } else { - obj.saveState = SAVE_STATE.FAILED; - if (kibanaObjectResult.error && kibanaObjectResult.error.message) { - obj.errors.push(kibanaObjectResult.error.message); - } - } - } else { - obj.saveState = SAVE_STATE.FAILED; - obj.errors.push( - i18n.translate('xpack.ml.newJob.simple.recognize.kibanaObject.couldNotSaveErrorMessage', { - defaultMessage: 'Could not save {objName} {objId}', - values: { objName, objId: obj.id } - }) - ); - } - $scope.$applyAsync(); - }); - }); - } - resolve(); - }) - .catch((err) => { - console.log('Error setting up module', err); - toastNotifications.addWarning({ - title: i18n.translate('xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningTitle', { - defaultMessage: 'Error setting up module {moduleId}', - values: { moduleId } - }), - text: i18n.translate('xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningDescription', { - defaultMessage: 'An error occurred trying to create the {count, plural, one {job} other {jobs}} in the module.', - values: { - count: $scope.formConfig.jobs.length - } - }) - }); - $scope.overallState = SAVE_STATE.FAILED; - $scope.$applyAsync(); - }); - }); - } - - // loop through all jobs, datafeeds and saved objects and set the save state to SAVING - function setAllToSaving() { - $scope.formConfig.jobs.forEach((j) => { - j.jobState = SAVE_STATE.SAVING; - j.datafeedState = SAVE_STATE.SAVING; - }); - - _.each($scope.formConfig.kibanaObjects, (kibanaObject) => { - kibanaObject.forEach((obj) => { - obj.saveState = SAVE_STATE.SAVING; - }); - }); - $scope.$applyAsync(); - } - - function startDatafeeds() { - return new Promise((resolve, reject) => { - - const jobs = $scope.formConfig.jobs; - const numberOfJobs = jobs.length; - - mlCreateRecognizerJobsService.indexTimeRange($scope.formConfig.indexPattern, $scope.formConfig) - .then((resp) => { - if ($scope.formConfig.useFullIndexData) { - $scope.formConfig.start = resp.start.epoch; - $scope.formConfig.end = resp.end.epoch; - } else { - $scope.formConfig.start = dateMath.parse(timefilter.getTime().from).valueOf(); - $scope.formConfig.end = dateMath.parse(timefilter.getTime().to).valueOf(); - } - let jobsCounter = 0; - let datafeedCounter = 0; - - open(jobs[jobsCounter]); - - function incrementAndOpen(job) { - jobsCounter++; - if (jobsCounter < numberOfJobs) { - open(jobs[jobsCounter]); - } else { - // if the last job failed, reject out of the function - // so it can be caught higher up - if (job.runningState === DATAFEED_STATE.FAILED) { - reject(); - } - } - } - - function open(job) { - if (job.jobState === SAVE_STATE.FAILED) { - // we're skipping over the datafeed, so bump the - // counter up manually so it all tallies at the end. - datafeedCounter++; - job.runningState = DATAFEED_STATE.FAILED; - incrementAndOpen(job); - return; - } - job.runningState = DATAFEED_STATE.STARTING; - const jobId = $scope.formConfig.jobLabel + job.id; - mlJobService.openJob(jobId) - .then(() => { - incrementAndOpen(job); - start(job); - }).catch((err) => { - console.log('Opening job failed', err); - start(job); - job.errors.push(err.message); - incrementAndOpen(job); - }); - } - - function start(job) { - const jobId = $scope.formConfig.jobLabel + job.id; - const datafeedId = prefixDatafeedId(job.datafeedId, $scope.formConfig.jobLabel); - mlCreateRecognizerJobsService.startDatafeed( - datafeedId, - jobId, - $scope.formConfig.start, - $scope.formConfig.end) - .then(() => { - job.runningState = DATAFEED_STATE.STARTED; - datafeedCounter++; - if (datafeedCounter === numberOfJobs) { - resolve(); - } - }) - .catch((err) => { - console.log('Starting datafeed failed', err); - job.errors.push(err.message); - job.runningState = DATAFEED_STATE.FAILED; - reject(err); - }) - .then(() => { - $scope.$applyAsync(); - }); - } - }); - }); - } - - - function checkIfKibanaObjectsExist(kibanaObjects) { - _.each(kibanaObjects, (objects, type) => { - objects.forEach((obj) => { - checkForSavedObject(type, obj) - .then((result) => { - if (result) { - obj.saveState = SAVE_STATE.SAVED; - obj.exists = true; - } - }); - }); - }); - } - - function checkForSavedObject(type, savedObject) { - return new Promise((resolve, reject) => { - let exists = false; - mlCreateRecognizerJobsService.loadExistingSavedObjects(type) - .then((resp) => { - const savedObjects = resp.savedObjects; - savedObjects.forEach((obj) => { - if (savedObject.title === obj.attributes.title) { - exists = true; - savedObject.id = obj.id; - } - }); - resolve(exists); - }).catch((resp) => { - console.log('Could not load saved objects', resp); - reject(resp); - }); - }); - } - - $scope.setOverallState = function () { - const jobIds = []; - const failedJobsCount = $scope.formConfig.jobs.reduce((count, job) => { - if (job.jobState === SAVE_STATE.FAILED || job.datafeedState === SAVE_STATE.FAILED) { - return count + 1; - } else { - jobIds.push(`${$scope.formConfig.jobLabel}${job.id}`); - return count; - } - }, 0); - - if (failedJobsCount) { - if (failedJobsCount === $scope.formConfig.jobs.length) { - $scope.overallState = SAVE_STATE.FAILED; - } else { - $scope.overallState = SAVE_STATE.PARTIAL_FAILURE; - } - } else { - $scope.overallState = SAVE_STATE.SAVED; - } - - $scope.resultsUrl = mlJobService.createResultsUrl( - jobIds, - $scope.formConfig.start, - $scope.formConfig.end, - 'explorer' - ); - - $scope.$applyAsync(); - }; - - - function validateJobs() { - let valid = true; - const checks = $scope.ui.validation.checks; - _.each(checks, (item) => { - item.valid = true; - }); - - // add an extra bit to the job label to avoid hitting the rule which states - // you can't have an id ending in a - or _ - // also to allow an empty label - const label = `${$scope.formConfig.jobLabel}extra`; - - - - if (isJobIdValid(label) === false) { - valid = false; - checks.jobLabel.valid = false; - const msg = i18n.translate('xpack.ml.newJob.simple.recognize.jobLabelAllowedCharactersDescription', { - defaultMessage: 'Job label can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' + - 'must start and end with an alphanumeric character' - }); - checks.jobLabel.message = msg; - } - $scope.formConfig.jobGroups.forEach(group => { - if (isJobIdValid(group) === false) { - valid = false; - checks.groupIds.valid = false; - const msg = i18n.translate('xpack.ml.newJob.simple.recognize.jobGroupAllowedCharactersDescription', { - defaultMessage: 'Job group names can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' + - 'must start and end with an alphanumeric character' - }); - checks.groupIds.message = msg; - } - }); - return valid; - } - - loadJobConfigs(); - - }); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_service.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_service.js deleted file mode 100644 index e0a5c3ef95df13..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_service.js +++ /dev/null @@ -1,51 +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 { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { ml } from 'plugins/ml/services/ml_api_service'; - -export function CreateRecognizerJobsServiceProvider(Private) { - - const savedObjectsClient = Private(SavedObjectsClientProvider); - class CreateRecognizerJobsService { - - constructor() {} - - createDatafeed(job, formConfig) { - return new Promise((resolve, reject) => { - const jobId = formConfig.jobLabel + job.id; - - mlJobService.saveNewDatafeed(job.datafeedConfig, jobId) - .then((resp) => { - resolve(resp); - }) - .catch((resp) => { - reject(resp); - }); - }); - } - - startDatafeed(datafeedId, jobId, start, end) { - return mlJobService.startDatafeed(datafeedId, jobId, start, end); - } - - loadExistingSavedObjects(type) { - return savedObjectsClient.find({ type, perPage: 1000 }); - } - - indexTimeRange(indexPattern, formConfig) { - const query = formConfig.combinedQuery; - return ml.getTimeFieldRange({ - index: indexPattern.title, - timeFieldName: indexPattern.timeFieldName, - query - }); - } - } - return new CreateRecognizerJobsService(); -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/index.ts similarity index 60% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js rename to x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/index.ts index 96b3791f4558c9..30059abf55cb1c 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/index.ts @@ -4,9 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './create_job_controller'; -import './create_job_service'; -import 'plugins/ml/services/mapping_service'; -import 'plugins/ml/components/job_group_select'; +export { JobGroupsInput } from './job_groups_input'; +export { TimeRangePicker, TimeRange } from './time_range_picker'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/job_groups_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/job_groups_input.tsx new file mode 100644 index 00000000000000..a71a264662fee6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/job_groups_input.tsx @@ -0,0 +1,80 @@ +/* + * 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, memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { Validation } from '../job_validator'; +import { tabColor } from '../../../../../common/util/group_color_utils'; +import { Description } from '../../pages/components/job_details_step/components/groups/description'; + +export interface JobGroupsInputProps { + existingGroups: string[]; + selectedGroups: string[]; + onChange: (value: string[]) => void; + validation: Validation; +} + +export const JobGroupsInput: FC = memo( + ({ existingGroups, selectedGroups, onChange, validation }) => { + const options = existingGroups.map(g => ({ + label: g, + color: tabColor(g), + })); + + const selectedOptions = selectedGroups.map(g => ({ + label: g, + color: tabColor(g), + })); + + function onChangeCallback(optionsIn: EuiComboBoxOptionProps[]) { + onChange(optionsIn.map(g => g.label)); + } + + function onCreateGroup(input: string, flattenedOptions: EuiComboBoxOptionProps[]) { + const normalizedSearchValue = input.trim().toLowerCase(); + + if (!normalizedSearchValue) { + return; + } + + const newGroup: EuiComboBoxOptionProps = { + label: input, + color: tabColor(input), + }; + + if ( + flattenedOptions.findIndex( + option => option.label.trim().toLowerCase() === normalizedSearchValue + ) === -1 + ) { + options.push(newGroup); + } + + onChangeCallback([...selectedOptions, newGroup]); + } + + return ( + + + + ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/time_range_picker.tsx similarity index 90% rename from x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx rename to x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/time_range_picker.tsx index 26140b9557e908..8c648696a9a7ae 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/time_range_picker.tsx @@ -4,23 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; -import React, { Fragment, FC, useState, useEffect } from 'react'; +import React, { FC, Fragment, useEffect, useState } from 'react'; +import moment, { Moment } from 'moment'; import { i18n } from '@kbn/i18n'; -import { EuiDatePickerRange, EuiDatePicker } from '@elastic/eui'; - -import { useKibanaContext } from '../../../../../contexts/kibana'; -import { TimeRange } from './time_range'; +import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; +import { useKibanaContext } from '../../../../contexts/kibana'; const WIDTH = '512px'; +export interface TimeRange { + start: number; + end: number; +} + interface Props { setTimeRange: (d: TimeRange) => void; timeRange: TimeRange; } -type Moment = moment.Moment; - export const TimeRangePicker: FC = ({ setTimeRange, timeRange }) => { const kibanaContext = useKibanaContext(); const dateFormat: string = kibanaContext.kibanaConfig.get('dateFormat'); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts index 5939da0d64abfa..688a56208d92b2 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReactElement } from 'react'; import { basicJobValidation } from '../../../../../common/util/job_utils'; import { newJobLimits } from '../../../new_job/utils/new_job_defaults'; import { JobCreatorType } from '../job_creator'; @@ -22,7 +23,7 @@ export interface ValidationSummary { export interface Validation { valid: boolean; - message?: string; + message?: string | ReactElement; } export interface BasicValidations { 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 f0061b1b9847eb..945d22967a65d6 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 @@ -10,3 +10,5 @@ import './pages/job_type/route'; import './pages/job_type/directive'; import './pages/index_or_search/route'; import './pages/index_or_search/directive'; +import './recognize/route'; +import './recognize/directive'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx index a1f0e1677bedba..a90ceb3ce27d5b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx @@ -4,25 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useContext, useState, useEffect } from 'react'; +import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { timefilter } from 'ui/timefilter'; import moment from 'moment'; import { WizardNav } from '../wizard_nav'; -import { WIZARD_STEPS, StepProps } from '../step_types'; +import { StepProps, WIZARD_STEPS } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { useKibanaContext } from '../../../../../contexts/kibana'; import { FullTimeRangeSelector } from '../../../../../components/full_time_range_selector'; import { EventRateChart } from '../charts/event_rate_chart'; import { LineChartPoint } from '../../../common/chart_loader'; -import { TimeRangePicker } from './time_range_picker'; import { GetTimeFieldRangeResponse } from '../../../../../services/ml_api_service'; +import { TimeRangePicker, TimeRange } from '../../../common/components'; -export interface TimeRange { - start: number; - end: number; -} export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) => { const kibanaContext = useKibanaContext(); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/__test__/directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/__test__/directive.js new file mode 100644 index 00000000000000..7cbf22bf45ec57 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/__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 - Recognize job 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 Recognize job 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/recognize/components/create_result_callout.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/create_result_callout.tsx new file mode 100644 index 00000000000000..9d0cf705aaba63 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/create_result_callout.tsx @@ -0,0 +1,103 @@ +/* + * 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, memo } from 'react'; +import { EuiCallOut, EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SAVE_STATE } from '../page'; + +interface CreateResultCalloutProps { + saveState: SAVE_STATE; + resultsUrl: string; + onReset: () => {}; +} + +export const CreateResultCallout: FC = memo( + ({ saveState, resultsUrl, onReset }) => { + if (saveState === SAVE_STATE.NOT_SAVED) { + return null; + } + return ( + <> + {saveState === SAVE_STATE.SAVED && ( + + } + color="success" + iconType="checkInCircleFilled" + /> + )} + {saveState === SAVE_STATE.FAILED && ( + + } + color="danger" + iconType="alert" + /> + )} + {saveState === SAVE_STATE.PARTIAL_FAILURE && ( + + } + color="warning" + iconType="alert" + /> + )} + + + {saveState !== SAVE_STATE.SAVING && ( + + + + + + )} + {(saveState === SAVE_STATE.SAVED || saveState === SAVE_STATE.PARTIAL_FAILURE) && ( + + + + + + )} + + + ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/job_settings_form.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/job_settings_form.tsx new file mode 100644 index 00000000000000..617f6b31e7e537 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/job_settings_form.tsx @@ -0,0 +1,323 @@ +/* + * 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, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiAccordion, + EuiButton, + EuiDescribedFormGroup, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiSwitch, + EuiTextAlign, +} from '@elastic/eui'; +import { ModuleJobUI, SAVE_STATE } from '../page'; +import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; +import { useKibanaContext } from '../../../../contexts/kibana'; +import { + composeValidators, + maxLengthValidator, + patternValidator, +} from '../../../../../common/util/validators'; +import { JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; +import { isJobIdValid } from '../../../../../common/util/job_utils'; +import { usePartialState } from '../../../../components/custom_hooks'; +import { JobGroupsInput, TimeRangePicker, TimeRange } from '../../common/components'; + +export interface JobSettingsFormValues { + jobPrefix: string; + startDatafeedAfterSave: boolean; + useFullIndexData: boolean; + timeRange: TimeRange; + useDedicatedIndex: boolean; + jobGroups: string[]; +} + +interface JobSettingsFormProps { + saveState: SAVE_STATE; + onSubmit: (values: JobSettingsFormValues) => any; + onChange: (values: JobSettingsFormValues) => any; + jobGroups: string[]; + existingGroupIds: string[]; + jobs: ModuleJobUI[]; +} + +export const JobSettingsForm: FC = ({ + onSubmit, + onChange, + saveState, + existingGroupIds, + jobs, + jobGroups, +}) => { + const { from, to } = getTimeFilterRange(); + const { currentIndexPattern: indexPattern } = useKibanaContext(); + + const jobPrefixValidator = composeValidators( + patternValidator(/^([a-z0-9]+[a-z0-9\-_]*)?$/), + maxLengthValidator(JOB_ID_MAX_LENGTH - Math.max(...jobs.map(({ id }) => id.length))) + ); + const groupValidator = composeValidators( + (value: string) => (isJobIdValid(value) ? null : { pattern: true }), + maxLengthValidator(JOB_ID_MAX_LENGTH) + ); + + const [formState, setFormState] = usePartialState({ + jobPrefix: '', + startDatafeedAfterSave: true, + useFullIndexData: true, + timeRange: { + start: from, + end: to, + }, + useDedicatedIndex: false, + jobGroups: [] as string[], + }); + const [validationResult, setValidationResult] = useState>({}); + + const onJobPrefixChange = (value: string) => { + setFormState({ + jobPrefix: value && value.toLowerCase(), + }); + }; + + const handleValidation = () => { + const jobPrefixValidationResult = jobPrefixValidator(formState.jobPrefix); + const jobGroupsValidationResult = formState.jobGroups + .map(group => groupValidator(group)) + .filter(result => result !== null); + + setValidationResult({ + jobPrefix: jobPrefixValidationResult, + jobGroups: jobGroupsValidationResult, + formValid: !jobPrefixValidationResult && jobGroupsValidationResult.length === 0, + }); + }; + + useEffect(() => { + handleValidation(); + }, [formState.jobPrefix, formState.jobGroups]); + + useEffect(() => { + onChange(formState); + }, [formState]); + + useEffect(() => { + setFormState({ jobGroups }); + }, [jobGroups]); + + return ( + <> + + + + + } + description={ + + } + > + + } + describedByIds={['ml_aria_label_new_job_recognizer_job_prefix']} + isInvalid={!!validationResult.jobPrefix} + error={ + <> + {validationResult.jobPrefix && validationResult.jobPrefix.maxLength ? ( +
+ +
+ ) : null} + {validationResult.jobPrefix && validationResult.jobPrefix.pattern && ( +
+ +
+ )} + + } + > + onJobPrefixChange(value)} + isInvalid={!!validationResult.jobPrefix} + /> +
+
+ { + setFormState({ + jobGroups: value, + }); + }} + validation={{ + valid: !validationResult.jobGroups || validationResult.jobGroups.length === 0, + message: ( + + ), + }} + /> + + + + } + checked={formState.startDatafeedAfterSave} + onChange={({ target: { checked } }) => { + setFormState({ + startDatafeedAfterSave: checked, + }); + }} + /> + + + + } + checked={formState.useFullIndexData} + onChange={({ target: { checked } }) => { + setFormState({ + useFullIndexData: checked, + }); + }} + /> + + {!formState.useFullIndexData && ( + <> + + { + setFormState({ + timeRange: value, + }); + }} + timeRange={formState.timeRange} + /> + + )} + + + } + paddingSize="l" + > + + + + } + description={ + + } + > + + { + setFormState({ + useDedicatedIndex: checked, + }); + }} + /> + + + + +
+ + { + onSubmit(formState); + }} + area-label={i18n.translate('xpack.ml.newJob.recognize.createJobButtonAriaLabel', { + defaultMessage: 'Create Job', + })} + > + {saveState === SAVE_STATE.NOT_SAVED && ( + + )} + {saveState === SAVE_STATE.SAVING && ( + + )} + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/kibana_objects.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/kibana_objects.tsx new file mode 100644 index 00000000000000..4954b44bf8842a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/kibana_objects.tsx @@ -0,0 +1,104 @@ +/* + * 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, memo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingSpinner, + EuiText, + EuiTitle, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { KibanaObjectUi } from '../page'; + +export interface KibanaObjectItemProps { + objectType: string; + kibanaObjects: KibanaObjectUi[]; + isSaving: boolean; +} + +export const KibanaObjects: FC = memo( + ({ objectType, kibanaObjects, isSaving }) => { + const kibanaObjectLabels: Record = { + dashboard: i18n.translate('xpack.ml.newJob.recognize.dashboardsLabel', { + defaultMessage: 'Dashboards', + }), + search: i18n.translate('xpack.ml.newJob.recognize.searchesLabel', { + defaultMessage: 'Searches', + }), + visualization: i18n.translate('xpack.ml.newJob.recognize.visualizationsLabel', { + defaultMessage: 'Visualizations', + }), + }; + + return ( + <> + +

{kibanaObjectLabels[objectType]}

+
+ +
    + {kibanaObjects.map(({ id, title, success, exists }, i) => ( +
  • + + + + + + {title} + + + {exists && ( + + + + + + )} + + + {!exists && ( + + + {isSaving ? : null} + {success !== undefined ? ( + + ) : null} + + + )} + + {(kibanaObjects.length === 1 || i < kibanaObjects.length - 1) && ( + + )} +
  • + ))} +
+ + ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/module_jobs.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/module_jobs.tsx new file mode 100644 index 00000000000000..76028510172e1c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/module_jobs.tsx @@ -0,0 +1,178 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiLoadingSpinner, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { ModuleJobUI, SAVE_STATE } from '../page'; + +interface ModuleJobsProps { + jobs: ModuleJobUI[]; + jobPrefix: string; + saveState: SAVE_STATE; +} + +const SETUP_RESULTS_WIDTH = '200px'; + +export const ModuleJobs: FC = ({ jobs, jobPrefix, saveState }) => { + const isSaving = saveState === SAVE_STATE.SAVING; + return ( + <> + +

+ +

+
+ + + + {saveState !== SAVE_STATE.SAVING && saveState !== SAVE_STATE.NOT_SAVED && ( + + + + + + + + + + + + + + + + + + + + + + )} + +
    + {jobs.map(({ id, config: { description }, setupResult, datafeedResult }, i) => ( +
  • + + + + {jobPrefix} + {id} + + + + {description} + + + {setupResult && setupResult.error && ( + + {setupResult.error.msg} + + )} + + {datafeedResult && datafeedResult.error && ( + + {datafeedResult.error.msg} + + )} + + + {isSaving && } + {setupResult && datafeedResult && ( + + + + + + + + + + + + + + )} + + + {i < jobs.length - 1 && } +
  • + ))} +
+ + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx new file mode 100644 index 00000000000000..593db836b5ddb7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx @@ -0,0 +1,69 @@ +/* + * 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('mlRecognizePage', ($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 $route = $injector.get('$route'); + + const moduleId = $route.current.params.id; + const existingGroupIds: string[] = $route.current.locals.existingJobsAndGroups.groupIds; + + const createSearchItems = Private(SearchItemsProvider); + const { indexPattern, savedSearch, combinedQuery } = createSearchItems(); + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kbnBaseUrl, + kibanaConfig, + }; + + ReactDOM.render( + + + {React.createElement(Page, { moduleId, existingGroupIds })} + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/page.tsx new file mode 100644 index 00000000000000..2c7600dcb99b20 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/page.tsx @@ -0,0 +1,314 @@ +/* + * 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, Fragment, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPage, + EuiPageBody, + EuiTitle, + EuiPageHeaderSection, + EuiPageHeader, + EuiFlexItem, + EuiFlexGroup, + EuiText, + EuiSpacer, + EuiCallOut, + EuiPanel, +} from '@elastic/eui'; +import { toastNotifications } from 'ui/notify'; +import { merge, flatten } from 'lodash'; +import { ml } from '../../../services/ml_api_service'; +import { useKibanaContext } from '../../../contexts/kibana'; +import { + DatafeedResponse, + DataRecognizerConfigResponse, + JobResponse, + KibanaObject, + KibanaObjectResponse, + Module, + ModuleJob, +} from '../../../../common/types/modules'; +import { mlJobService } from '../../../services/job_service'; +import { CreateResultCallout } from './components/create_result_callout'; +import { KibanaObjects } from './components/kibana_objects'; +import { ModuleJobs } from './components/module_jobs'; +import { checkForSavedObjects } from './resolvers'; +import { JobSettingsForm, JobSettingsFormValues } from './components/job_settings_form'; +import { TimeRange } from '../common/components'; + +export interface ModuleJobUI extends ModuleJob { + datafeedResult?: DatafeedResponse; + setupResult?: JobResponse; +} + +export type KibanaObjectUi = KibanaObject & KibanaObjectResponse; + +export interface KibanaObjects { + [objectType: string]: KibanaObjectUi[]; +} + +interface PageProps { + moduleId: string; + existingGroupIds: string[]; +} + +export enum SAVE_STATE { + NOT_SAVED, + SAVING, + SAVED, + FAILED, + PARTIAL_FAILURE, +} + +export const Page: FC = ({ moduleId, existingGroupIds }) => { + // #region State + const [jobPrefix, setJobPrefix] = useState(''); + const [jobs, setJobs] = useState([]); + const [jobGroups, setJobGroups] = useState([]); + const [kibanaObjects, setKibanaObjects] = useState({}); + const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED); + const [resultsUrl, setResultsUrl] = useState(''); + // #endregion + + const { + currentSavedSearch: savedSearch, + currentIndexPattern: indexPattern, + combinedQuery, + } = useKibanaContext(); + const pageTitle = + savedSearch.id !== undefined + ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', { + defaultMessage: 'saved search {savedSearchTitle}', + values: { savedSearchTitle: savedSearch.title }, + }) + : i18n.translate('xpack.ml.newJob.recognize.indexPatternPageTitle', { + defaultMessage: 'index pattern {indexPatternTitle}', + values: { indexPatternTitle: indexPattern.title }, + }); + const displayQueryWarning = savedSearch.id !== undefined; + const tempQuery = savedSearch.id === undefined ? undefined : combinedQuery; + + const loadModule = async () => { + try { + const response: Module = await ml.getDataRecognizerModule({ moduleId }); + setJobs(response.jobs); + + const kibanaObjectsResult = await checkForSavedObjects(response.kibana as KibanaObjects); + setKibanaObjects(kibanaObjectsResult); + + setJobGroups([ + ...new Set(flatten(response.jobs.map(({ config: { groups = [] } }) => groups))), + ]); + setSaveState(SAVE_STATE.NOT_SAVED); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + }; + + const getTimeRange = async ( + useFullIndexData: boolean, + timeRange: TimeRange + ): Promise => { + if (useFullIndexData) { + const { start, end } = await ml.getTimeFieldRange({ + index: indexPattern.title, + timeFieldName: indexPattern.timeFieldName, + query: combinedQuery, + }); + return { + start: start.epoch, + end: end.epoch, + }; + } else { + return Promise.resolve(timeRange); + } + }; + + useEffect(() => { + loadModule(); + }, []); + + const save = async (formValues: JobSettingsFormValues) => { + setSaveState(SAVE_STATE.SAVING); + const { + jobPrefix: resultJobPrefix, + jobGroups: resultJobGroups, + startDatafeedAfterSave, + useDedicatedIndex, + useFullIndexData, + timeRange, + } = formValues; + + const resultTimeRange = await getTimeRange(useFullIndexData, timeRange); + + try { + const response: DataRecognizerConfigResponse = await ml.setupDataRecognizerConfig({ + moduleId, + prefix: resultJobPrefix, + groups: resultJobGroups, + query: tempQuery, + indexPatternName: indexPattern.title, + useDedicatedIndex, + startDatafeed: startDatafeedAfterSave, + ...resultTimeRange, + }); + const { datafeeds: datafeedsResponse, jobs: jobsResponse, kibana: kibanaResponse } = response; + + setJobs( + jobs.map(job => { + return { + ...job, + datafeedResult: datafeedsResponse.find(({ id }) => id.endsWith(job.id)), + setupResult: jobsResponse.find(({ id }) => id === resultJobPrefix + job.id), + }; + }) + ); + setKibanaObjects(merge(kibanaObjects, kibanaResponse)); + setResultsUrl( + mlJobService.createResultsUrl( + jobsResponse.filter(({ success }) => success).map(({ id }) => id), + resultTimeRange.start, + resultTimeRange.end, + 'explorer' + ) + ); + const failedJobsCount = jobsResponse.reduce((count, { success }) => { + return success ? count : count + 1; + }, 0); + setSaveState( + failedJobsCount === 0 + ? SAVE_STATE.SAVED + : failedJobsCount === jobs.length + ? SAVE_STATE.FAILED + : SAVE_STATE.PARTIAL_FAILURE + ); + } catch (e) { + setSaveState(SAVE_STATE.FAILED); + // eslint-disable-next-line no-console + console.error('Error setting up module', e); + toastNotifications.addDanger({ + title: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningTitle', { + defaultMessage: 'Error setting up module {moduleId}', + values: { moduleId }, + }), + text: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningDescription', { + defaultMessage: + 'An error occurred trying to create the {count, plural, one {job} other {jobs}} in the module.', + values: { + count: jobs.length, + }, + }), + }); + } + }; + + const isFormVisible = [SAVE_STATE.NOT_SAVED, SAVE_STATE.SAVING].includes(saveState); + + return ( + + + + + +

+ +

+
+
+
+ + {displayQueryWarning && ( + <> + + } + color="warning" + iconType="alert" + > + + + + + + + )} + + + + + +

+ +

+
+ + + + {isFormVisible && ( + { + setJobPrefix(formValues.jobPrefix); + }} + existingGroupIds={existingGroupIds} + saveState={saveState} + jobs={jobs} + jobGroups={jobGroups} + /> + )} + +
+
+ + + + + {Object.keys(kibanaObjects).length > 0 && ( + <> + + + {Object.keys(kibanaObjects).map((objectType, i) => ( + + + {i < Object.keys(kibanaObjects).length - 1 && } + + ))} + + + )} + +
+ +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/resolvers.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/resolvers.ts new file mode 100644 index 00000000000000..d92ec7152adf89 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/resolvers.ts @@ -0,0 +1,95 @@ +/* + * 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 chrome from 'ui/chrome'; +import { i18n } from '@kbn/i18n'; +import { toastNotifications } from 'ui/notify'; +import { IPrivate } from 'ui/private'; +import { mlJobService } from '../../../services/job_service'; +import { ml } from '../../../services/ml_api_service'; +import { KibanaObjects } from './page'; + +/** + * Checks whether the jobs in a data recognizer module have been created. + * Redirects to the Anomaly Explorer to view the jobs if they have been created, + * or the recognizer job wizard for the module if not. + */ +export function checkViewOrCreateJobs( + Private: IPrivate, + $route: any, + kbnBaseUrl: string, + kbnUrl: any +) { + return new Promise((resolve, reject) => { + const moduleId = $route.current.params.id; + const indexPatternId = $route.current.params.index; + + // Load the module, and check if the job(s) in the module have been created. + // If so, load the jobs in the Anomaly Explorer. + // Otherwise open the data recognizer wizard for the module. + // Always want to call reject() so as not to load original page. + ml.dataRecognizerModuleJobsExist({ moduleId }) + .then((resp: any) => { + const basePath = `${chrome.getBasePath()}/app/`; + + if (resp.jobsExist === true) { + const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer'); + window.location.href = `${basePath}${resultsPageUrl}`; + reject(); + } else { + window.location.href = `${basePath}ml#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; + reject(); + } + }) + .catch((err: Error) => { + // eslint-disable-next-line no-console + console.error(`Error checking whether jobs in module ${moduleId} exists`, err); + toastNotifications.addWarning({ + title: i18n.translate('xpack.ml.newJob.recognize.moduleCheckJobsExistWarningTitle', { + defaultMessage: 'Error checking module {moduleId}', + values: { moduleId }, + }), + text: i18n.translate('xpack.ml.newJob.recognize.moduleCheckJobsExistWarningDescription', { + defaultMessage: + 'An error occurred trying to check whether the jobs in the module have been created.', + }), + }); + + kbnUrl.redirect(`/jobs`); + reject(); + }); + }); +} + +/** + * Gets kibana objects with an existence check. + */ +export const checkForSavedObjects = async (objects: KibanaObjects): Promise => { + const savedObjectsClient = chrome.getSavedObjectsClient(); + try { + return await Object.keys(objects).reduce(async (prevPromise, type) => { + const acc = await prevPromise; + const { savedObjects } = await savedObjectsClient.find({ + type, + perPage: 1000, + }); + + acc[type] = objects[type].map(obj => { + const find = savedObjects.find(savedObject => savedObject.attributes.title === obj.title); + return { + ...obj, + exists: !!find, + id: (!!find && find.id) || obj.id, + }; + }); + return Promise.resolve(acc); + }, Promise.resolve({} as KibanaObjects)); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Could not load saved objects', e); + } + return Promise.resolve(objects); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/route.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/route.ts new file mode 100644 index 00000000000000..3d18d39d93734f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/route.ts @@ -0,0 +1,36 @@ +/* + * 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 'plugins/ml/license/check_license'; +import { getCreateRecognizerJobBreadcrumbs } from '../../breadcrumbs'; +import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; +import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../util/index_utils'; +import { mlJobService } from '../../../services/job_service'; +import { checkViewOrCreateJobs } from './resolvers'; + +uiRoutes.when('/jobs/new_job/recognize', { + template: '', + k7Breadcrumbs: getCreateRecognizerJobBreadcrumbs, + resolve: { + CheckLicense: checkLicenseExpired, + privileges: checkCreateJobsPrivilege, + indexPattern: loadCurrentIndexPattern, + savedSearch: loadCurrentSavedSearch, + checkMlNodesAvailable, + existingJobsAndGroups: mlJobService.getJobAndGroupIds, + }, +}); + +uiRoutes.when('/modules/check_view_or_create', { + template: '', + resolve: { + checkViewOrCreateJobs, + }, +}); diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/services/job_service.d.ts index 3b60a7af505d7d..c2d3882580be1b 100644 --- a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/job_service.d.ts @@ -11,7 +11,7 @@ export interface ExistingJobsAndGroups { declare interface JobService { currentJob: any; - createResultsUrlForJobs: () => string; + createResultsUrlForJobs: (jobs: any[], target: string) => string; tempJobCloningObjects: { job: any; skipTimeRangeStep: boolean; diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts index 475e723f9fbc4d..6f0194664db7c8 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts @@ -73,7 +73,9 @@ declare interface Ml { getDatafeedStats(obj: object): Promise; esSearch(obj: object): any; getIndices(): Promise; - + dataRecognizerModuleJobsExist(obj: { moduleId: string }): Promise; + getDataRecognizerModule(obj: { moduleId: string }): Promise; + setupDataRecognizerConfig(obj: object): Promise; getTimeFieldRange(obj: object): Promise; calculateModelMemoryLimit(obj: object): Promise<{ modelMemoryLimit: string }>; calendars(): Promise< diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js index b153aa4e07f68a..cd5f7ba3c8cda6 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js @@ -290,7 +290,10 @@ export const ml = { 'groups', 'indexPatternName', 'query', - 'useDedicatedIndex' + 'useDedicatedIndex', + 'startDatafeed', + 'start', + 'end' ]); return http({ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d75c730d5e25de..d8ecd623bc9bfe 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7006,75 +7006,6 @@ "xpack.ml.newJob.simple.postSaveOptions.couldNotStartDatafeedErrorMessage": "データフィードを開始できませんでした", "xpack.ml.newJob.simple.postSaveOptions.createWatchButtonAriaLabel": "ウォッチを作成", "xpack.ml.newJob.simple.postSaveOptions.createWatchForRealTimeJobLabel": "リアルタイムジョブのウォッチを作成", - "xpack.ml.newJob.simple.recognize.advancedLabel": "高度な設定", - "xpack.ml.newJob.simple.recognize.advancedSettingsAriaLabel": "高度な設定", - "xpack.ml.newJob.simple.recognize.alreadyExistsLabel": "(既に存在します)", - "xpack.ml.newJob.simple.recognize.analysisRunningAriaLabel": "分析を実行中", - "xpack.ml.newJob.simple.recognize.analysisRunningLabel": "分析を実行中", - "xpack.ml.newJob.simple.recognize.createJobButtonAriaLabel": "ジョブの作成", - "xpack.ml.newJob.simple.recognize.createJobButtonLabel": "{numberOfJobs, plural, zero {Job} one {Job} other {Jobs}} を作成", - "xpack.ml.newJob.simple.recognize.dashboardsLabel": "ダッシュボード", - "xpack.ml.newJob.simple.recognize.datafeed.couldNotSaveDatafeedErrorMessage": "データフィード {datafeedId} を保存できませんでした", - "xpack.ml.newJob.simple.recognize.datafeed.notSavedAriaLabel": "保存されていません", - "xpack.ml.newJob.simple.recognize.datafeed.savedAriaLabel": "保存されました", - "xpack.ml.newJob.simple.recognize.datafeed.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.datafeed.savingAriaLabel": "保存中", - "xpack.ml.newJob.simple.recognize.datafeedLabel": "データフィード", - "xpack.ml.newJob.simple.recognize.hideAdvancedButtonAriaLabel": "高度な設定を非表示", - "xpack.ml.newJob.simple.recognize.indexPatternPageTitle": "インデックスパターン {indexPatternTitle}", - "xpack.ml.newJob.simple.recognize.job.couldNotSaveJobErrorMessage": "ジョブ {jobId} を保存できませんでした", - "xpack.ml.newJob.simple.recognize.job.notSavedAriaLabel": "保存されていません", - "xpack.ml.newJob.simple.recognize.job.savedAriaLabel": "保存されました", - "xpack.ml.newJob.simple.recognize.job.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.job.savingAriaLabel": "保存中", - "xpack.ml.newJob.simple.recognize.jobDetailsTitle": "ジョブの詳細", - "xpack.ml.newJob.simple.recognize.jobGroupAllowedCharactersDescription": "ジョブグループ名にはアルファベットの小文字 (a-z と 0-9)、ハイフンまたはアンダーラインが使用でき、最初と最後を英数字にする必要があります", - "xpack.ml.newJob.simple.recognize.jobGroupsLabel": "ジョブグループ", - "xpack.ml.newJob.simple.recognize.jobIdPrefixLabel": "ジョブ ID の接頭辞", - "xpack.ml.newJob.simple.recognize.jobIdPrefixPlaceholder": "ジョブ ID の接頭辞", - "xpack.ml.newJob.simple.recognize.jobLabel": "ジョブ", - "xpack.ml.newJob.simple.recognize.jobLabelAllowedCharactersDescription": "ジョブラベルにはアルファベットの小文字 (a-z と 0-9)、ハイフンまたはアンダーラインが使用でき、最初と最後を英数字にする必要があります", - "xpack.ml.newJob.simple.recognize.jobsCreatedTitle": "ジョブが作成されました", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.resetButtonAriaLabel": "リセット", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.resetButtonLabel": "ジョブの作成に失敗", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.jobsCreationFailedTitle": "ジョブの作成に失敗", - "xpack.ml.newJob.simple.recognize.jobSettingsTitle": "ジョブ設定", - "xpack.ml.newJob.simple.recognize.jobsTitle": "ジョブ", - "xpack.ml.newJob.simple.recognize.kibanaObject.couldNotSaveErrorMessage": "{objName} {objId} を保存できませんでした", - "xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningDescription": "モジュールのジョブがクラッシュしたか確認する際にエラーが発生しました。", - "xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningTitle": "モジュール {moduleId} の確認中にエラーが発生", - "xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningDescription": "モジュールでの{count, plural, one {ジョブ} other {件のジョブ}}の作成中にエラーが発生しました。", - "xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningTitle": "モジュール {moduleId} のセットアップ中にエラーが発生", - "xpack.ml.newJob.simple.recognize.newJobFromTitle": "{pageTitle} からの新規ジョブ", - "xpack.ml.newJob.simple.recognize.results.alreadySavedAriaLabel": "既に保存済み", - "xpack.ml.newJob.simple.recognize.results.notSavedAriaLabel": "保存されていません", - "xpack.ml.newJob.simple.recognize.results.savedAriaLabel": "保存されました", - "xpack.ml.newJob.simple.recognize.results.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.results.savingAriaLabel": "保存中", - "xpack.ml.newJob.simple.recognize.running.notStartedAriaLabel": "未開始", - "xpack.ml.newJob.simple.recognize.running.startedAriaLabel": "開始済み", - "xpack.ml.newJob.simple.recognize.running.startFailedAriaLabel": "開始に失敗", - "xpack.ml.newJob.simple.recognize.running.startingAriaLabel": "開始中", - "xpack.ml.newJob.simple.recognize.runningLabel": "実行中", - "xpack.ml.newJob.simple.recognize.savedAriaLabel": "保存されました", - "xpack.ml.newJob.simple.recognize.savedSearchPageTitle": "保存された検索 {savedSearchTitle}", - "xpack.ml.newJob.simple.recognize.searchesLabel": "検索", - "xpack.ml.newJob.simple.recognize.searchWillBeOverwrittenLabel": "検索は上書きされます", - "xpack.ml.newJob.simple.recognize.showAdvancedAriaLabel": "高度な設定を表示", - "xpack.ml.newJob.simple.recognize.showAdvancedButtonAriaLabel": "高度な設定を表示", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.resetButtonAriaLabel": "リセット", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.resetButtonLabel": "リセット", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailedTitle": "一部のジョブの作成に失敗しました", - "xpack.ml.newJob.simple.recognize.startDatafeedAfterSaveLabel": "保存後データフィードを開始", - "xpack.ml.newJob.simple.recognize.useDedicatedIndexAriaLabel": "専用インデックスを使用", - "xpack.ml.newJob.simple.recognize.useDedicatedIndexLabel": "専用インデックスを使用", - "xpack.ml.newJob.simple.recognize.useFullDataLabel": "完全な {indexPatternTitle} データを使用", - "xpack.ml.newJob.simple.recognize.usingSavedSearchDescription": "保存された検索を使用すると、データフィードで使用されるクエリが、{moduleId} モジュールでデフォルトで提供されるものと異なるものになります。", - "xpack.ml.newJob.simple.recognize.viewResultsAriaLabel": "結果を表示", - "xpack.ml.newJob.simple.recognize.viewResultsLinkText": "結果を表示", - "xpack.ml.newJob.simple.recognize.visualizationsLabel": "ビジュアライゼーション", "xpack.ml.newJob.simple.singleMetric.advancedConfigurationLinkText": "高度なジョブの構成に移動", "xpack.ml.newJob.simple.singleMetric.advancedLabel": "高度な設定", "xpack.ml.newJob.simple.singleMetric.aggregationLabel": "集約", @@ -7441,7 +7372,6 @@ "xpack.ml.tooltips.newCustomUrlValueTooltip": "ドリルスルーリンクの URL です。分析されたフィールドの文字列置換をサポートします (例: {hostnameParam})。", "xpack.ml.tooltips.newFilterRuleActionTooltip": "ルールアクションを指定する文字列です。初めは有効なオプションが「filter_results」だけですが、プロビジョニングにより「disable_modeling」のようなアクションに拡張されます。", "xpack.ml.tooltips.newFilterTargetFieldNameTooltip": "フィールド名を入力する文字列です。フィルターは ruleConditions が適用される targetFieldName 値のすべての結果に適用されます。未入力の場合、フィルターは ruleConditions が適用される結果にのみ適用されます。", - "xpack.ml.tooltips.newJobAdvancedSettingsTooltip": "高度なオプション", "xpack.ml.tooltips.newJobBucketSpanTooltip": "時系列分析の間隔です。", "xpack.ml.tooltips.newJobCategorizationFieldNameTooltip": "オプション。非構造化ログデータの分析用。テキストデータタイプの使用をお勧めします。", "xpack.ml.tooltips.newJobCategorizationFiltersTooltip": "オプション。カテゴリー分けフィールドに正規表現を適用します", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6e450431cc28e0..aa18442168806a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7008,75 +7008,6 @@ "xpack.ml.newJob.simple.postSaveOptions.couldNotStartDatafeedErrorMessage": "无法开始数据馈送:", "xpack.ml.newJob.simple.postSaveOptions.createWatchButtonAriaLabel": "创建监视", "xpack.ml.newJob.simple.postSaveOptions.createWatchForRealTimeJobLabel": "为实时作业创建监视", - "xpack.ml.newJob.simple.recognize.advancedLabel": "高级", - "xpack.ml.newJob.simple.recognize.advancedSettingsAriaLabel": "高级设置", - "xpack.ml.newJob.simple.recognize.alreadyExistsLabel": "(已存在)", - "xpack.ml.newJob.simple.recognize.analysisRunningAriaLabel": "分析正在运行", - "xpack.ml.newJob.simple.recognize.analysisRunningLabel": "分析正在运行", - "xpack.ml.newJob.simple.recognize.createJobButtonAriaLabel": "创建作业", - "xpack.ml.newJob.simple.recognize.createJobButtonLabel": "创建 {numberOfJobs, plural, zero { 个作业} one {Job} other {Jobs}}", - "xpack.ml.newJob.simple.recognize.dashboardsLabel": "仪表板", - "xpack.ml.newJob.simple.recognize.datafeed.couldNotSaveDatafeedErrorMessage": "无法保存数据馈送 {datafeedId}", - "xpack.ml.newJob.simple.recognize.datafeed.notSavedAriaLabel": "未保存", - "xpack.ml.newJob.simple.recognize.datafeed.savedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.datafeed.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.datafeed.savingAriaLabel": "正在保存", - "xpack.ml.newJob.simple.recognize.datafeedLabel": "数据馈送", - "xpack.ml.newJob.simple.recognize.hideAdvancedButtonAriaLabel": "隐藏“高级”", - "xpack.ml.newJob.simple.recognize.indexPatternPageTitle": "索引模式 {indexPatternTitle}", - "xpack.ml.newJob.simple.recognize.job.couldNotSaveJobErrorMessage": "无法保存作业 {jobId}", - "xpack.ml.newJob.simple.recognize.job.notSavedAriaLabel": "未保存", - "xpack.ml.newJob.simple.recognize.job.savedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.job.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.job.savingAriaLabel": "正在保存", - "xpack.ml.newJob.simple.recognize.jobDetailsTitle": "作业详情", - "xpack.ml.newJob.simple.recognize.jobGroupAllowedCharactersDescription": "作业组名称可以包含小写字母数字(a-z 和 0-9)、连字符或下划线;必须以字母数字字符开头和结尾", - "xpack.ml.newJob.simple.recognize.jobGroupsLabel": "作业组", - "xpack.ml.newJob.simple.recognize.jobIdPrefixLabel": "作业 ID 前缀", - "xpack.ml.newJob.simple.recognize.jobIdPrefixPlaceholder": "作业 ID 前缀", - "xpack.ml.newJob.simple.recognize.jobLabel": "作业", - "xpack.ml.newJob.simple.recognize.jobLabelAllowedCharactersDescription": "作业标签可以包含小写字母数字(a-z 和 0-9)、连字符或下划线;必须以字母数字字符开头和结尾", - "xpack.ml.newJob.simple.recognize.jobsCreatedTitle": "已创建作业", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.resetButtonAriaLabel": "重置", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.resetButtonLabel": "作业创建失败", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.jobsCreationFailedTitle": "作业创建失败", - "xpack.ml.newJob.simple.recognize.jobSettingsTitle": "作业设置", - "xpack.ml.newJob.simple.recognize.jobsTitle": "作业", - "xpack.ml.newJob.simple.recognize.kibanaObject.couldNotSaveErrorMessage": "无法保存 {objName} {objId}", - "xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningDescription": "尝试检查模块中的作业是否已创建时出错。", - "xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningTitle": "检查模式 {moduleId} 时出错", - "xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningDescription": "尝试在模块中创建{count, plural, one {该作业} other {这些作业}}时出错。", - "xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningTitle": "设置模块 {moduleId} 时出错", - "xpack.ml.newJob.simple.recognize.newJobFromTitle": "来自 {pageTitle} 的新作业", - "xpack.ml.newJob.simple.recognize.results.alreadySavedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.results.notSavedAriaLabel": "未保存", - "xpack.ml.newJob.simple.recognize.results.savedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.results.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.results.savingAriaLabel": "正在保存", - "xpack.ml.newJob.simple.recognize.running.notStartedAriaLabel": "未开始", - "xpack.ml.newJob.simple.recognize.running.startedAriaLabel": "已开始", - "xpack.ml.newJob.simple.recognize.running.startFailedAriaLabel": "启动失败", - "xpack.ml.newJob.simple.recognize.running.startingAriaLabel": "正在启动", - "xpack.ml.newJob.simple.recognize.runningLabel": "正在运行", - "xpack.ml.newJob.simple.recognize.savedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.savedSearchPageTitle": "已保存搜索 {savedSearchTitle}", - "xpack.ml.newJob.simple.recognize.searchesLabel": "搜索", - "xpack.ml.newJob.simple.recognize.searchWillBeOverwrittenLabel": "搜索将被覆盖", - "xpack.ml.newJob.simple.recognize.showAdvancedAriaLabel": "显示“高级”", - "xpack.ml.newJob.simple.recognize.showAdvancedButtonAriaLabel": "显示“高级”", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.resetButtonAriaLabel": "重置", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.resetButtonLabel": "重置", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailedTitle": "部分作业未能创建", - "xpack.ml.newJob.simple.recognize.startDatafeedAfterSaveLabel": "保存后开始数据馈送", - "xpack.ml.newJob.simple.recognize.useDedicatedIndexAriaLabel": "使用专用索引", - "xpack.ml.newJob.simple.recognize.useDedicatedIndexLabel": "使用专用索引", - "xpack.ml.newJob.simple.recognize.useFullDataLabel": "使用完整的 {indexPatternTitle} 数据", - "xpack.ml.newJob.simple.recognize.usingSavedSearchDescription": "使用保存的搜索意味着在数据馈送中使用的查询会与我们在 {moduleId} 模块中提供的默认查询不同。", - "xpack.ml.newJob.simple.recognize.viewResultsAriaLabel": "查看结果", - "xpack.ml.newJob.simple.recognize.viewResultsLinkText": "查看结果", - "xpack.ml.newJob.simple.recognize.visualizationsLabel": "可视化", "xpack.ml.newJob.simple.singleMetric.advancedConfigurationLinkText": "转到高级作业配置", "xpack.ml.newJob.simple.singleMetric.advancedLabel": "高级", "xpack.ml.newJob.simple.singleMetric.aggregationLabel": "聚合", @@ -7443,7 +7374,6 @@ "xpack.ml.tooltips.newCustomUrlValueTooltip": "钻取链接的 URL。支持已分析字段(例如 {hostnameParam})的字符串替代。", "xpack.ml.tooltips.newFilterRuleActionTooltip": "指定规则操作的字符串。初始,唯一有效选项是“filter_results”,但其通过配置可扩展操作,例如“disable_modeling”。", "xpack.ml.tooltips.newFilterTargetFieldNameTooltip": "应为字段名称的字符串。筛选将应用于 ruleConditions 应用的 targetFieldName 值的所有结果。为空时,筛选将应用于 ruleConditions 适用的结果。", - "xpack.ml.tooltips.newJobAdvancedSettingsTooltip": "高级选项", "xpack.ml.tooltips.newJobBucketSpanTooltip": "时间序列分析的时间间隔。", "xpack.ml.tooltips.newJobCategorizationFieldNameTooltip": "(可选)用于分析非结构化日志数据。建议使用文本数据类型。", "xpack.ml.tooltips.newJobCategorizationFiltersTooltip": "(可选)将正则表达式应用于分类字段",