From 546146c4bf8ccd520bb7ecd749cc1826271493fe Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 28 Aug 2019 00:05:33 -0700 Subject: [PATCH] [ML] Data frame analytics: Advanced editor. (#43989) Adds an option to switch to an advanced (JSON) editor when creating an analytics job. This builds upon the previous work for the modal for analytics job creation and the use of useReducer(): - The files of the custom hook useCreateAnalyticsForm() have been further split up and there's now separate actions.ts and state.ts files. - To only allow updating what's really related to the form value's state via setFormState, the state structure has been updated and more fine grained actions have been added. - The user can enabled the advanced editor, but cannot move back to the original form (there's a help text in the UI about that). - The advanced editor component's (CreateAnalyticsAdvancedEditor) structure is based on the regular form, it still has an input field for the job ID and the toggle for optionally creating an index pattern. The fields for source/destination index are replaced by an editable JSON textarea input. - The advanced editor features mostly the same validation like the regular form. For example, if the source index isn't valid, an error will be shown in a CallOut below the editable area. --- .../legacy/plugins/ml/common/types/common.ts | 6 + .../data_frame_analytics/common/analytics.ts | 10 +- .../data_frame_analytics/common/index.ts | 1 - .../components/exploration/exploration.tsx | 2 +- .../analytics_list/analytics_list.tsx | 33 +- .../create_analytics_advanced_editor.tsx | 184 +++++++++++ .../create_analytics_advanced_editor/index.ts | 7 + .../create_analytics_button.tsx | 15 +- .../create_analytics_form.tsx | 41 ++- .../create_analytics_modal.tsx | 14 +- .../use_create_analytics_form/actions.ts | 77 +++++ .../use_create_analytics_form/reducer.test.ts | 4 +- .../use_create_analytics_form/reducer.ts | 304 ++++++++++-------- .../use_create_analytics_form/state.test.ts | 23 ++ .../hooks/use_create_analytics_form/state.ts | 107 ++++++ .../use_create_analytics_form.test.tsx | 4 +- .../use_create_analytics_form.ts | 139 +++++--- .../pages/analytics_management/page.tsx | 10 +- 18 files changed, 750 insertions(+), 231 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts diff --git a/x-pack/legacy/plugins/ml/common/types/common.ts b/x-pack/legacy/plugins/ml/common/types/common.ts index 0dff3979140c0..3f3493863e0f5 100644 --- a/x-pack/legacy/plugins/ml/common/types/common.ts +++ b/x-pack/legacy/plugins/ml/common/types/common.ts @@ -13,3 +13,9 @@ export interface Dictionary { export function dictionaryToArray(dict: Dictionary): TValue[] { return Object.keys(dict).map(key => dict[key]); } + +// A recursive partial type to allow passing nested partial attributes. +// Used for example for the optional `jobConfig.dest.results_field` property. +export type DeepPartial = { + [P in keyof T]?: DeepPartial; +}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts index ccd925f22846a..eb09c79b60355 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts @@ -21,17 +21,17 @@ export type IndexName = string; export type IndexPattern = string; export type DataFrameAnalyticsId = string; -export interface CreateRequestBody { +export interface DataFrameAnalyticsOutlierConfig { + id: DataFrameAnalyticsId; // Description attribute is not supported yet // description?: string; dest: { index: IndexName; results_field: string; }; -} - -export interface DataFrameAnalyticsOutlierConfig extends CreateRequestBody { - id: DataFrameAnalyticsId; + source: { + index: IndexName; + }; analysis: { outlier_detection: {}; }; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts index c9cf20281c23f..e5f7866aaf376 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts @@ -9,7 +9,6 @@ export { moveToAnalyticsWizard, refreshAnalyticsList$, useRefreshAnalyticsList, - CreateRequestBody, DataFrameAnalyticsId, DataFrameAnalyticsOutlierConfig, IndexName, diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx index 48bd31c34dcfa..3dca88cca2d6e 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx @@ -86,7 +86,7 @@ const ExplorationTitle: React.SFC<{ jobId: string }> = ({ jobId }) => ( {i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { - defaultMessage: 'Job id {jobId}', + defaultMessage: 'Job ID {jobId}', values: { jobId }, })} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 078ddc452fb46..24819399ae2b7 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -16,11 +16,7 @@ import { SortDirection, } from '@elastic/eui'; -import { - DataFrameAnalyticsId, - moveToAnalyticsWizard, - useRefreshAnalyticsList, -} from '../../../../common'; +import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common'; import { checkPermission } from '../../../../../privilege/check_privilege'; import { getTaskStateBadge } from './columns'; @@ -33,6 +29,7 @@ import { Query, Clause, } from './common'; +import { ActionDispatchers } from '../../hooks/use_create_analytics_form/actions'; import { getAnalyticsFactory } from '../../services/analytics_service'; import { getColumns } from './columns'; import { ExpandedRow } from './expanded_row'; @@ -65,11 +62,13 @@ function stringMatch(str: string | undefined, substr: string) { interface Props { isManagementTable?: boolean; blockRefresh?: boolean; + openCreateJobModal?: ActionDispatchers['openModal']; } // isManagementTable - for use in Kibana managagement ML section export const DataFrameAnalyticsList: FC = ({ isManagementTable = false, blockRefresh = false, + openCreateJobModal, }) => { const [isInitialized, setIsInitialized] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -208,21 +207,21 @@ export const DataFrameAnalyticsList: FC = ({ title={

{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', { - defaultMessage: 'No data frame analytics found', + defaultMessage: 'No data frame analytics jobs found', })}

} - actions={[ - - {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', { - defaultMessage: 'Create your first data frame analytics', - })} - , - ]} + actions={ + !isManagementTable && openCreateJobModal !== undefined + ? [ + + {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', { + defaultMessage: 'Create your first data frame analytics job', + })} + , + ] + : [] + } data-test-subj="mlNoDataFrameAnalyticsFound" /> diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx new file mode 100644 index 0000000000000..05689a9e048f6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -0,0 +1,184 @@ +/* + * 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, Fragment } from 'react'; + +import { + EuiCallOut, + EuiCodeEditor, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; + +export const CreateAnalyticsAdvancedEditor: FC = ({ actions, state }) => { + const { + resetAdvancedEditorMessages, + setAdvancedEditorRawString, + setFormState, + setJobConfig, + } = actions; + + const { advancedEditorMessages, advancedEditorRawString, isJobCreated, requestMessages } = state; + + const { + createIndexPattern, + destinationIndexPatternTitleExists, + jobId, + jobIdEmpty, + jobIdExists, + jobIdValid, + } = state.form; + + const onChange = (str: string) => { + setAdvancedEditorRawString(str); + try { + setJobConfig(JSON.parse(str)); + } catch (e) { + resetAdvancedEditorMessages(); + } + }; + + return ( + + {requestMessages.map((requestMessage, i) => ( + + + {requestMessage.error !== undefined ?

{requestMessage.error}

: null} +
+ +
+ ))} + {!isJobCreated && ( + + + setFormState({ jobId: e.target.value })} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditor.jobIdInputAriaLabel', + { + defaultMessage: 'Choose a unique analytics job ID.', + } + )} + isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists} + /> + + + + + + {advancedEditorMessages.map((advancedEditorMessage, i) => ( + + + {advancedEditorMessage.message !== '' && + advancedEditorMessage.error !== undefined ? ( +

{advancedEditorMessage.error}

+ ) : null} +
+ +
+ ))} + + setFormState({ createIndexPattern: !createIndexPattern })} + /> + +
+ )} +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts new file mode 100644 index 0000000000000..cf046b7f2ae58 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CreateAnalyticsAdvancedEditor } from './create_analytics_advanced_editor'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index 20b9df34b4aaf..dd48fdd2fea5a 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -12,15 +12,15 @@ import { i18n } from '@kbn/i18n'; import { createPermissionFailureMessage } from '../../../../../privilege/check_privilege'; -import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; +import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; +import { CreateAnalyticsAdvancedEditor } from '../create_analytics_advanced_editor'; import { CreateAnalyticsForm } from '../create_analytics_form'; import { CreateAnalyticsModal } from '../create_analytics_modal'; -export const CreateAnalyticsButton: FC = () => { - const { state, actions } = useCreateAnalyticsForm(); - const { disabled, isModalVisible } = state; - const { openModal } = actions; +export const CreateAnalyticsButton: FC = props => { + const { disabled, isAdvancedEditorEnabled, isModalVisible } = props.state; + const { openModal } = props.actions; const button = ( { {button} {isModalVisible && ( - - + + {isAdvancedEditorEnabled === false && } + {isAdvancedEditorEnabled === true && } )} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 873a762276b07..8f5629b640fce 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -7,6 +7,7 @@ import React, { Fragment, FC } from 'react'; import { + EuiButtonEmpty, EuiCallOut, EuiComboBox, EuiForm, @@ -15,6 +16,7 @@ import { EuiLink, EuiSpacer, EuiSwitch, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -23,8 +25,17 @@ import { metadata } from 'ui/metadata'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -export const CreateAnalyticsForm: FC = ({ actions, formState }) => { +export const CreateAnalyticsForm: FC = ({ actions, state }) => { const { setFormState } = actions; + + const { + form, + indexPatternsWithNumericFields, + indexPatternTitles, + isJobCreated, + requestMessages, + } = state; + const { createIndexPattern, destinationIndex, @@ -32,19 +43,15 @@ export const CreateAnalyticsForm: FC = ({ actions, for destinationIndexNameExists, destinationIndexNameValid, destinationIndexPatternTitleExists, - indexPatternsWithNumericFields, - indexPatternTitles, - isJobCreated, jobId, jobIdEmpty, jobIdExists, jobIdValid, - requestMessages, sourceIndex, sourceIndexNameEmpty, sourceIndexNameExists, sourceIndexNameValid, - } = formState; + } = form; return ( @@ -65,7 +72,7 @@ export const CreateAnalyticsForm: FC = ({ actions, for = ({ actions, for ...(jobIdExists ? [ i18n.translate('xpack.ml.dataframe.analytics.create.jobIdExistsError', { - defaultMessage: 'An analytics job with this id already exists.', + defaultMessage: 'An analytics job with this ID already exists.', }), ] : []), @@ -88,19 +95,18 @@ export const CreateAnalyticsForm: FC = ({ actions, for > setFormState({ jobId: e.target.value })} aria-label={i18n.translate( 'xpack.ml.dataframe.analytics.create.jobIdInputAriaLabel', { - defaultMessage: 'Choose a unique analytics job id.', + defaultMessage: 'Choose a unique analytics job ID.', } )} isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists} /> - = ({ actions, for )} - = ({ actions, for isInvalid={!destinationIndexNameEmpty && !destinationIndexNameValid} /> - = ({ actions, for onChange={() => setFormState({ createIndexPattern: !createIndexPattern })} /> + + {i18n.translate('xpack.ml.dataframe.analytics.create.switchToAdvancedEditorButton', { + defaultMessage: 'Switch to advanced editor', + })} + + + {i18n.translate('xpack.ml.dataframe.analytics.create.switchToAdvancedEditorHelpText', { + defaultMessage: + 'Note you cannot switch back to this form once the advanced editor is enabled.', + })} + )} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx index 2f5f9c944ef65..05a5bf3524392 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx @@ -24,14 +24,22 @@ import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form' export const CreateAnalyticsModal: FC = ({ actions, children, - formState, + state, }) => { const { closeModal, createAnalyticsJob, startAnalyticsJob } = actions; - const { isJobCreated, isJobStarted, isModalButtonDisabled, isValid } = formState; + const { + isAdvancedEditorEnabled, + isJobCreated, + isJobStarted, + isModalButtonDisabled, + isValid, + } = state; + + const width = isAdvancedEditorEnabled ? '640px' : '450px'; return ( - + {i18n.translate('xpack.ml.dataframe.analytics.create.modalHeaderTitle', { diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts new file mode 100644 index 0000000000000..f9d11605b167a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts @@ -0,0 +1,77 @@ +/* + * 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 { FormMessage, State } from './state'; + +export enum ACTION { + ADD_REQUEST_MESSAGE, + RESET_REQUEST_MESSAGES, + CLOSE_MODAL, + OPEN_MODAL, + RESET_ADVANCED_EDITOR_MESSAGES, + RESET_FORM, + SET_ADVANCED_EDITOR_RAW_STRING, + SET_FORM_STATE, + SET_INDEX_NAMES, + SET_INDEX_PATTERN_TITLES, + SET_IS_JOB_CREATED, + SET_IS_JOB_STARTED, + SET_IS_MODAL_BUTTON_DISABLED, + SET_IS_MODAL_VISIBLE, + SET_JOB_CONFIG, + SET_JOB_IDS, + SWITCH_TO_ADVANCED_EDITOR, +} + +export type Action = + // Actions which only consist of the action type and no payload: + | { + type: + | ACTION.RESET_REQUEST_MESSAGES + | ACTION.CLOSE_MODAL + | ACTION.OPEN_MODAL + | ACTION.RESET_ADVANCED_EDITOR_MESSAGES + | ACTION.RESET_FORM + | ACTION.SWITCH_TO_ADVANCED_EDITOR; + } + // Actions with custom payloads: + | { type: ACTION.ADD_REQUEST_MESSAGE; requestMessage: FormMessage } + | { + type: ACTION.SET_ADVANCED_EDITOR_RAW_STRING; + advancedEditorRawString: State['advancedEditorRawString']; + } + | { type: ACTION.SET_FORM_STATE; payload: Partial } + | { type: ACTION.SET_INDEX_NAMES; indexNames: State['indexNames'] } + | { + type: ACTION.SET_INDEX_PATTERN_TITLES; + payload: { + indexPatternTitles: State['indexPatternTitles']; + indexPatternsWithNumericFields: State['indexPatternsWithNumericFields']; + }; + } + | { type: ACTION.SET_IS_JOB_CREATED; isJobCreated: State['isJobCreated'] } + | { type: ACTION.SET_IS_JOB_STARTED; isJobStarted: State['isJobStarted'] } + | { + type: ACTION.SET_IS_MODAL_BUTTON_DISABLED; + isModalButtonDisabled: State['isModalButtonDisabled']; + } + | { type: ACTION.SET_IS_MODAL_VISIBLE; isModalVisible: State['isModalVisible'] } + | { type: ACTION.SET_JOB_CONFIG; payload: State['jobConfig'] } + | { type: ACTION.SET_JOB_IDS; jobIds: State['jobIds'] }; + +// Actions wrapping the dispatcher exposed by the custom hook +export interface ActionDispatchers { + closeModal: () => void; + createAnalyticsJob: () => void; + openModal: () => void; + resetAdvancedEditorMessages: () => void; + setAdvancedEditorRawString: (payload: State['advancedEditorRawString']) => void; + setFormState: (payload: Partial) => void; + setIsModalVisible: (payload: State['isModalVisible']) => void; + setJobConfig: (payload: State['jobConfig']) => void; + startAnalyticsJob: () => void; + switchToAdvancedEditor: () => void; +} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts index 8324f41872d4d..891a181eeb4c1 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getInitialState, reducer, ACTION } from './reducer'; +import { ACTION } from './actions'; +import { reducer } from './reducer'; +import { getInitialState } from './state'; describe('useCreateAnalyticsForm', () => { test('reducer(): provide a minimum required valid job config, then reset.', () => { diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 68fd46a69fee5..19b050c024f17 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -4,113 +4,122 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isValidIndexName } from '../../../../../../common/util/es_utils'; -import { checkPermission } from '../../../../../privilege/check_privilege'; - -import { isAnalyticsIdValid, DataFrameAnalyticsId } from '../../../../common'; +import { idx } from '@kbn/elastic-idx'; +import { i18n } from '@kbn/i18n'; -export type EsIndexName = string; -export type IndexPatternTitle = string; +import { isValidIndexName } from '../../../../../../common/util/es_utils'; -export interface RequestMessage { - error?: string; - message: string; -} +import { isAnalyticsIdValid } from '../../../../common'; + +import { Action, ACTION } from './actions'; +import { getInitialState, getJobConfigFromFormState, State } from './state'; + +const validateAdvancedEditor = (state: State): State => { + const { jobIdEmpty, jobIdValid, jobIdExists, createIndexPattern } = state.form; + const { jobConfig } = state; + + state.advancedEditorMessages = []; + + const sourceIndexName = idx(jobConfig, _ => _.source.index) || ''; + const sourceIndexNameEmpty = sourceIndexName === ''; + const sourceIndexNameValid = isValidIndexName(sourceIndexName); + + const destinationIndexName = idx(jobConfig, _ => _.dest.index) || ''; + const destinationIndexNameEmpty = destinationIndexName === ''; + const destinationIndexNameValid = isValidIndexName(destinationIndexName); + const destinationIndexPatternTitleExists = state.indexPatternTitles.some( + name => destinationIndexName === name + ); + + if (sourceIndexNameEmpty) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameEmpty', + { + defaultMessage: 'The source index name must not be empty.', + } + ), + message: '', + }); + } else if (!sourceIndexNameValid) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameValid', + { + defaultMessage: 'Invalid source index name.', + } + ), + message: '', + }); + } -export interface State { - createIndexPattern: boolean; - destinationIndex: EsIndexName; - destinationIndexNameExists: boolean; - destinationIndexNameEmpty: boolean; - destinationIndexNameValid: boolean; - destinationIndexPatternTitleExists: boolean; - disabled: boolean; - indexNames: EsIndexName[]; - indexPatternTitles: IndexPatternTitle[]; - indexPatternsWithNumericFields: IndexPatternTitle[]; - isJobCreated: boolean; - isJobStarted: boolean; - isModalButtonDisabled: boolean; - isModalVisible: boolean; - isValid: boolean; - jobId: DataFrameAnalyticsId; - jobIds: DataFrameAnalyticsId[]; - jobIdExists: boolean; - jobIdEmpty: boolean; - jobIdValid: boolean; - requestMessages: RequestMessage[]; - sourceIndex: EsIndexName; - sourceIndexNameExists: boolean; - sourceIndexNameEmpty: boolean; - sourceIndexNameValid: boolean; -} + if (destinationIndexNameEmpty) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameEmpty', + { + defaultMessage: 'The destination index name must not be empty.', + } + ), + message: '', + }); + } else if (!destinationIndexNameValid) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameValid', + { + defaultMessage: 'Invalid destination index name.', + } + ), + message: '', + }); + } -export const getInitialState = (): State => ({ - createIndexPattern: false, - destinationIndex: '', - destinationIndexNameExists: false, - destinationIndexNameEmpty: true, - destinationIndexNameValid: false, - destinationIndexPatternTitleExists: false, - disabled: - !checkPermission('canCreateDataFrameAnalytics') || - !checkPermission('canStartStopDataFrameAnalytics'), - indexNames: [], - indexPatternTitles: [], - indexPatternsWithNumericFields: [], - isJobCreated: false, - isJobStarted: false, - isModalVisible: false, - isModalButtonDisabled: false, - isValid: false, - jobId: '', - jobIds: [], - jobIdExists: false, - jobIdEmpty: true, - jobIdValid: false, - requestMessages: [], - sourceIndex: '', - sourceIndexNameExists: false, - sourceIndexNameEmpty: true, - sourceIndexNameValid: false, -}); - -const validate = (state: State): State => { state.isValid = - !state.jobIdEmpty && - state.jobIdValid && - !state.jobIdExists && - !state.sourceIndexNameEmpty && - state.sourceIndexNameValid && - !state.destinationIndexNameEmpty && - state.destinationIndexNameValid && - (!state.destinationIndexPatternTitleExists || !state.createIndexPattern); + !jobIdEmpty && + jobIdValid && + !jobIdExists && + !sourceIndexNameEmpty && + sourceIndexNameValid && + !destinationIndexNameEmpty && + destinationIndexNameValid && + (!destinationIndexPatternTitleExists || !createIndexPattern); return state; }; -export enum ACTION { - ADD_REQUEST_MESSAGE = 'add_request_message', - RESET_REQUEST_MESSAGES = 'reset_request_messages', - CLOSE_MODAL = 'close_modal', - OPEN_MODAL = 'open_modal', - RESET_FORM = 'reset_form', - SET_FORM_STATE = 'set_form_state', -} +const validateForm = (state: State): State => { + const { + jobIdEmpty, + jobIdValid, + jobIdExists, + sourceIndexNameEmpty, + sourceIndexNameValid, + destinationIndexNameEmpty, + destinationIndexNameValid, + destinationIndexPatternTitleExists, + createIndexPattern, + } = state.form; + + state.isValid = + !jobIdEmpty && + jobIdValid && + !jobIdExists && + !sourceIndexNameEmpty && + sourceIndexNameValid && + !destinationIndexNameEmpty && + destinationIndexNameValid && + (!destinationIndexPatternTitleExists || !createIndexPattern); -export type Action = - | { type: ACTION.ADD_REQUEST_MESSAGE; requestMessage: RequestMessage } - | { type: ACTION.RESET_REQUEST_MESSAGES } - | { type: ACTION.CLOSE_MODAL } - | { type: ACTION.OPEN_MODAL } - | { type: ACTION.RESET_FORM } - | { type: ACTION.SET_FORM_STATE; payload: Partial }; + return state; +}; export function reducer(state: State, action: Action): State { switch (action.type) { case ACTION.ADD_REQUEST_MESSAGE: - state.requestMessages.push(action.requestMessage); - return state; + const requestMessages = state.requestMessages; + requestMessages.push(action.requestMessage); + return { ...state, requestMessages }; case ACTION.RESET_REQUEST_MESSAGES: return { ...state, requestMessages: [] }; @@ -121,58 +130,99 @@ export function reducer(state: State, action: Action): State { case ACTION.OPEN_MODAL: return { ...state, isModalVisible: true }; + case ACTION.RESET_ADVANCED_EDITOR_MESSAGES: + return { ...state, advancedEditorMessages: [] }; + case ACTION.RESET_FORM: return getInitialState(); + case ACTION.SET_ADVANCED_EDITOR_RAW_STRING: + return { ...state, advancedEditorRawString: action.advancedEditorRawString }; + case ACTION.SET_FORM_STATE: - const newState = { ...state, ...action.payload }; + const newFormState = { ...state.form, ...action.payload }; // update state attributes which are derived from other state attributes. if (action.payload.destinationIndex !== undefined) { - newState.destinationIndexNameExists = newState.indexNames.some( - name => newState.destinationIndex === name - ); - newState.destinationIndexNameEmpty = newState.destinationIndex === ''; - newState.destinationIndexNameValid = isValidIndexName(newState.destinationIndex); - newState.destinationIndexPatternTitleExists = newState.indexPatternTitles.some( - name => newState.destinationIndex === name - ); - } - - if (action.payload.indexNames !== undefined) { - newState.destinationIndexNameExists = newState.indexNames.some( - name => newState.destinationIndex === name + newFormState.destinationIndexNameExists = state.indexNames.some( + name => newFormState.destinationIndex === name ); - newState.sourceIndexNameExists = newState.indexNames.some( - name => newState.sourceIndex === name - ); - } - - if (action.payload.indexPatternTitles !== undefined) { - newState.destinationIndexPatternTitleExists = newState.indexPatternTitles.some( - name => newState.destinationIndex === name + newFormState.destinationIndexNameEmpty = newFormState.destinationIndex === ''; + newFormState.destinationIndexNameValid = isValidIndexName(newFormState.destinationIndex); + newFormState.destinationIndexPatternTitleExists = state.indexPatternTitles.some( + name => newFormState.destinationIndex === name ); } if (action.payload.jobId !== undefined) { - newState.jobIdExists = newState.jobIds.some(id => newState.jobId === id); - newState.jobIdEmpty = newState.jobId === ''; - newState.jobIdValid = isAnalyticsIdValid(newState.jobId); - } - - if (action.payload.jobIds !== undefined) { - newState.jobIdExists = newState.jobIds.some(id => newState.jobId === id); + newFormState.jobIdExists = state.jobIds.some(id => newFormState.jobId === id); + newFormState.jobIdEmpty = newFormState.jobId === ''; + newFormState.jobIdValid = isAnalyticsIdValid(newFormState.jobId); } if (action.payload.sourceIndex !== undefined) { - newState.sourceIndexNameExists = newState.indexNames.some( - name => newState.sourceIndex === name + newFormState.sourceIndexNameExists = state.indexNames.some( + name => newFormState.sourceIndex === name ); - newState.sourceIndexNameEmpty = newState.sourceIndex === ''; - newState.sourceIndexNameValid = isValidIndexName(newState.sourceIndex); + newFormState.sourceIndexNameEmpty = newFormState.sourceIndex === ''; + newFormState.sourceIndexNameValid = isValidIndexName(newFormState.sourceIndex); } - return validate(newState); + return state.isAdvancedEditorEnabled + ? validateAdvancedEditor({ ...state, form: newFormState }) + : validateForm({ ...state, form: newFormState }); + + case ACTION.SET_INDEX_NAMES: { + const newState = { ...state, indexNames: action.indexNames }; + newState.form.destinationIndexNameExists = newState.indexNames.some( + name => newState.form.destinationIndex === name + ); + newState.form.sourceIndexNameExists = newState.indexNames.some( + name => newState.form.sourceIndex === name + ); + return newState; + } + + case ACTION.SET_INDEX_PATTERN_TITLES: { + const newState = { + ...state, + ...action.payload, + }; + newState.form.destinationIndexPatternTitleExists = newState.indexPatternTitles.some( + name => newState.form.destinationIndex === name + ); + return newState; + } + + case ACTION.SET_IS_JOB_CREATED: + return { ...state, isJobCreated: action.isJobCreated }; + + case ACTION.SET_IS_JOB_STARTED: + return { ...state, isJobStarted: action.isJobStarted }; + + case ACTION.SET_IS_MODAL_BUTTON_DISABLED: + return { ...state, isModalButtonDisabled: action.isModalButtonDisabled }; + + case ACTION.SET_IS_MODAL_VISIBLE: + return { ...state, isModalVisible: action.isModalVisible }; + + case ACTION.SET_JOB_CONFIG: + return validateAdvancedEditor({ ...state, jobConfig: action.payload }); + + case ACTION.SET_JOB_IDS: { + const newState = { ...state, jobIds: action.jobIds }; + newState.form.jobIdExists = newState.jobIds.some(id => newState.form.jobId === id); + return newState; + } + + case ACTION.SWITCH_TO_ADVANCED_EDITOR: + const jobConfig = getJobConfigFromFormState(state.form); + return validateAdvancedEditor({ + ...state, + advancedEditorRawString: JSON.stringify(jobConfig, null, 2), + isAdvancedEditorEnabled: true, + jobConfig, + }); } return state; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts new file mode 100644 index 0000000000000..7d7c89960200d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { idx } from '@kbn/elastic-idx'; + +import { getInitialState, getJobConfigFromFormState } from './state'; + +describe('useCreateAnalyticsForm', () => { + test('state: getJobConfigFromFormState()', () => { + const state = getInitialState(); + + state.form.destinationIndex = 'the-destination-index'; + state.form.sourceIndex = 'the-source-index'; + + const jobConfig = getJobConfigFromFormState(state.form); + + expect(idx(jobConfig, _ => _.dest.index)).toBe('the-destination-index'); + expect(idx(jobConfig, _ => _.source.index)).toBe('the-source-index'); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts new file mode 100644 index 0000000000000..8a28409947528 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -0,0 +1,107 @@ +/* + * 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 { DeepPartial } from '../../../../../../common/types/common'; +import { checkPermission } from '../../../../../privilege/check_privilege'; + +import { DataFrameAnalyticsId, DataFrameAnalyticsOutlierConfig } from '../../../../common'; + +const ANALYTICS_DETAULT_MODEL_MEMORY_LIMIT = '50mb'; + +export type EsIndexName = string; +export type IndexPatternTitle = string; + +export interface FormMessage { + error?: string; + message: string; +} + +export interface State { + advancedEditorMessages: FormMessage[]; + advancedEditorRawString: string; + form: { + createIndexPattern: boolean; + destinationIndex: EsIndexName; + destinationIndexNameExists: boolean; + destinationIndexNameEmpty: boolean; + destinationIndexNameValid: boolean; + destinationIndexPatternTitleExists: boolean; + jobId: DataFrameAnalyticsId; + jobIdExists: boolean; + jobIdEmpty: boolean; + jobIdValid: boolean; + sourceIndex: EsIndexName; + sourceIndexNameExists: boolean; + sourceIndexNameEmpty: boolean; + sourceIndexNameValid: boolean; + }; + disabled: boolean; + indexNames: EsIndexName[]; + indexPatternTitles: IndexPatternTitle[]; + indexPatternsWithNumericFields: IndexPatternTitle[]; + isAdvancedEditorEnabled: boolean; + isJobCreated: boolean; + isJobStarted: boolean; + isModalButtonDisabled: boolean; + isModalVisible: boolean; + isValid: boolean; + jobConfig: DeepPartial; + jobIds: DataFrameAnalyticsId[]; + requestMessages: FormMessage[]; +} + +export const getInitialState = (): State => ({ + advancedEditorMessages: [], + advancedEditorRawString: '', + form: { + createIndexPattern: false, + destinationIndex: '', + destinationIndexNameExists: false, + destinationIndexNameEmpty: true, + destinationIndexNameValid: false, + destinationIndexPatternTitleExists: false, + jobId: '', + jobIdExists: false, + jobIdEmpty: true, + jobIdValid: false, + sourceIndex: '', + sourceIndexNameExists: false, + sourceIndexNameEmpty: true, + sourceIndexNameValid: false, + }, + jobConfig: {}, + disabled: + !checkPermission('canCreateDataFrameAnalytics') || + !checkPermission('canStartStopDataFrameAnalytics'), + indexNames: [], + indexPatternTitles: [], + indexPatternsWithNumericFields: [], + isAdvancedEditorEnabled: false, + isJobCreated: false, + isJobStarted: false, + isModalVisible: false, + isModalButtonDisabled: false, + isValid: false, + jobIds: [], + requestMessages: [], +}); + +export const getJobConfigFromFormState = ( + formState: State['form'] +): DeepPartial => { + return { + source: { + index: formState.sourceIndex, + }, + dest: { + index: formState.destinationIndex, + }, + analysis: { + outlier_detection: {}, + }, + model_memory_limit: ANALYTICS_DETAULT_MODEL_MEMORY_LIMIT, + }; +}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx index 4985c369ea2c6..3298a7d00253f 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx @@ -57,7 +57,7 @@ describe('useCreateAnalyticsForm()', () => { act(() => { // this should be actions.openModal(), but that doesn't work yet because act() doesn't support async yet. // we need to wait for an update to React 16.9 - actions.setFormState({ isModalVisible: true }); + actions.setIsModalVisible(true); }); const { state: stateModalOpen } = getLastHookValue(); expect(stateModalOpen.isModalVisible).toBe(true); @@ -65,7 +65,7 @@ describe('useCreateAnalyticsForm()', () => { act(() => { // this should be actions.closeModal(), but that doesn't work yet because act() doesn't support async yet. // we need to wait for an update to React 16.9 - actions.setFormState({ isModalVisible: false }); + actions.setIsModalVisible(false); }); const { state: stateModalClosed } = getLastHookValue(); expect(stateModalClosed.isModalVisible).toBe(false); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 1205b9650be87..0bff0f31c0faa 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -11,28 +11,26 @@ import { i18n } from '@kbn/i18n'; import { ml } from '../../../../../services/ml_api_service'; import { useKibanaContext } from '../../../../../contexts/kibana'; -import { useRefreshAnalyticsList, DataFrameAnalyticsOutlierConfig } from '../../../../common'; +import { + useRefreshAnalyticsList, + DataFrameAnalyticsId, + DataFrameAnalyticsOutlierConfig, +} from '../../../../common'; +import { ActionDispatchers, ACTION } from './actions'; +import { reducer } from './reducer'; import { getInitialState, - reducer, + getJobConfigFromFormState, + EsIndexName, + FormMessage, IndexPatternTitle, - RequestMessage, State, - ACTION, -} from './reducer'; - -export interface Actions { - closeModal: () => void; - createAnalyticsJob: () => void; - openModal: () => void; - startAnalyticsJob: () => void; - setFormState: (payload: Partial) => void; -} +} from './state'; export interface CreateAnalyticsFormProps { - actions: Actions; - formState: State; + actions: ActionDispatchers; + state: State; } // List of system fields we want to ignore for the numeric field check. @@ -51,29 +49,55 @@ export const useCreateAnalyticsForm = () => { const [state, dispatch] = useReducer(reducer, getInitialState()); const { refresh } = useRefreshAnalyticsList(); - const { createIndexPattern, destinationIndex, jobId, sourceIndex } = state; + const { form, jobConfig, isAdvancedEditorEnabled } = state; + const { createIndexPattern, destinationIndex, jobId } = form; - const addRequestMessage = (requestMessage: RequestMessage) => + const addRequestMessage = (requestMessage: FormMessage) => dispatch({ type: ACTION.ADD_REQUEST_MESSAGE, requestMessage }); + const closeModal = () => dispatch({ type: ACTION.CLOSE_MODAL }); + + const resetAdvancedEditorMessages = () => + dispatch({ type: ACTION.RESET_ADVANCED_EDITOR_MESSAGES }); + + const setIndexNames = (indexNames: EsIndexName[]) => + dispatch({ type: ACTION.SET_INDEX_NAMES, indexNames }); + + const setAdvancedEditorRawString = (advancedEditorRawString: string) => + dispatch({ type: ACTION.SET_ADVANCED_EDITOR_RAW_STRING, advancedEditorRawString }); + + const setIndexPatternTitles = (payload: { + indexPatternTitles: IndexPatternTitle[]; + indexPatternsWithNumericFields: IndexPatternTitle[]; + }) => dispatch({ type: ACTION.SET_INDEX_PATTERN_TITLES, payload }); + + const setIsJobCreated = (isJobCreated: boolean) => + dispatch({ type: ACTION.SET_IS_JOB_CREATED, isJobCreated }); + + const setIsJobStarted = (isJobStarted: boolean) => { + dispatch({ type: ACTION.SET_IS_JOB_STARTED, isJobStarted }); + }; + + const setIsModalButtonDisabled = (isModalButtonDisabled: boolean) => + dispatch({ type: ACTION.SET_IS_MODAL_BUTTON_DISABLED, isModalButtonDisabled }); + + const setIsModalVisible = (isModalVisible: boolean) => + dispatch({ type: ACTION.SET_IS_MODAL_VISIBLE, isModalVisible }); + + const setJobIds = (jobIds: DataFrameAnalyticsId[]) => + dispatch({ type: ACTION.SET_JOB_IDS, jobIds }); + const resetRequestMessages = () => dispatch({ type: ACTION.RESET_REQUEST_MESSAGES }); + const resetForm = () => dispatch({ type: ACTION.RESET_FORM }); const createAnalyticsJob = async () => { resetRequestMessages(); - setFormState({ isModalButtonDisabled: true }); - - const analyticsJobConfig = { - source: { - index: sourceIndex, - }, - dest: { - index: destinationIndex, - }, - analysis: { - outlier_detection: {}, - }, - }; + setIsModalButtonDisabled(true); + + const analyticsJobConfig = isAdvancedEditorEnabled + ? jobConfig + : getJobConfigFromFormState(form); try { await ml.dataFrameAnalytics.createDataFrameAnalytics(jobId, analyticsJobConfig); @@ -86,7 +110,8 @@ export const useCreateAnalyticsForm = () => { } ), }); - setFormState({ isJobCreated: true, isModalButtonDisabled: false }); + setIsModalButtonDisabled(false); + setIsJobCreated(true); if (createIndexPattern) { createKibanaIndexPattern(); } @@ -101,9 +126,7 @@ export const useCreateAnalyticsForm = () => { } ), }); - setFormState({ - isModalButtonDisabled: false, - }); + setIsModalButtonDisabled(false); } }; @@ -173,25 +196,25 @@ export const useCreateAnalyticsForm = () => { // re-fetch existing analytics job IDs and indices for form validation try { - setFormState({ - jobIds: (await ml.dataFrameAnalytics.getDataFrameAnalytics()).data_frame_analytics.map( + setJobIds( + (await ml.dataFrameAnalytics.getDataFrameAnalytics()).data_frame_analytics.map( (job: DataFrameAnalyticsOutlierConfig) => job.id - ), - }); + ) + ); } catch (e) { addRequestMessage({ error: getErrorMessage(e), message: i18n.translate( 'xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList', { - defaultMessage: 'An error occurred getting the existing data frame analytics job Ids:', + defaultMessage: 'An error occurred getting the existing data frame analytics job IDs:', } ), }); } try { - setFormState({ indexNames: (await ml.getIndices()).map(index => index.name) }); + setIndexNames((await ml.getIndices()).map(index => index.name)); } catch (e) { addRequestMessage({ error: getErrorMessage(e), @@ -206,12 +229,12 @@ export const useCreateAnalyticsForm = () => { try { // Set the index pattern titles which the user can choose as the source. - setFormState({ indexPatternTitles: await kibanaContext.indexPatterns.getTitles(true) }); + const indexPatternTitles = await kibanaContext.indexPatterns.getTitles(true); // Find out which index patterns contain numeric fields. // This will be used to provide a hint in the form that an analytics jobs is not // able to identify outliers if there are no numeric fields present. const ids = await kibanaContext.indexPatterns.getIds(true); - const newIndexPatternsWithNumericFields: IndexPatternTitle[] = []; + const indexPatternsWithNumericFields: IndexPatternTitle[] = []; ids.forEach(async id => { const indexPattern = await kibanaContext.indexPatterns.get(id); if ( @@ -220,12 +243,10 @@ export const useCreateAnalyticsForm = () => { .map(f => f.type) .includes('number') ) { - newIndexPatternsWithNumericFields.push(indexPattern.title); + indexPatternsWithNumericFields.push(indexPattern.title); } }); - setFormState({ - indexPatternsWithNumericFields: newIndexPatternsWithNumericFields, - }); + setIndexPatternTitles({ indexPatternTitles, indexPatternsWithNumericFields }); } catch (e) { addRequestMessage({ error: getErrorMessage(e), @@ -242,7 +263,7 @@ export const useCreateAnalyticsForm = () => { }; const startAnalyticsJob = async () => { - setFormState({ isModalButtonDisabled: true }); + setIsModalButtonDisabled(true); try { const response = await ml.dataFrameAnalytics.startDataFrameAnalytics(jobId); if (response.acknowledged !== true) { @@ -257,7 +278,8 @@ export const useCreateAnalyticsForm = () => { } ), }); - setFormState({ isJobStarted: true, isModalButtonDisabled: false }); + setIsJobStarted(true); + setIsModalButtonDisabled(false); refresh(); } catch (e) { addRequestMessage({ @@ -269,20 +291,33 @@ export const useCreateAnalyticsForm = () => { } ), }); - setFormState({ isModalButtonDisabled: false }); + setIsModalButtonDisabled(false); } }; - const setFormState = (payload: Partial) => { + const setJobConfig = (payload: Record) => { + dispatch({ type: ACTION.SET_JOB_CONFIG, payload }); + }; + + const setFormState = (payload: Partial) => { dispatch({ type: ACTION.SET_FORM_STATE, payload }); }; - const actions: Actions = { + const switchToAdvancedEditor = () => { + dispatch({ type: ACTION.SWITCH_TO_ADVANCED_EDITOR }); + }; + + const actions: ActionDispatchers = { closeModal, createAnalyticsJob, openModal, - startAnalyticsJob, + resetAdvancedEditorMessages, + setAdvancedEditorRawString, setFormState, + setIsModalVisible, + setJobConfig, + startAnalyticsJob, + switchToAdvancedEditor, }; return { state, actions }; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx index f84b22276ce69..c846a196dafab 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx @@ -28,12 +28,15 @@ import { CreateAnalyticsButton } from './components/create_analytics_button'; import { DataFrameAnalyticsList } from './components/analytics_list'; import { RefreshAnalyticsListButton } from './components/refresh_analytics_list_button'; import { useRefreshInterval } from './components/analytics_list/use_refresh_interval'; +import { useCreateAnalyticsForm } from './hooks/use_create_analytics_form'; export const Page: FC = () => { const [blockRefresh, setBlockRefresh] = useState(false); useRefreshInterval(setBlockRefresh); + const createAnalyticsForm = useCreateAnalyticsForm(); + return ( @@ -73,7 +76,7 @@ export const Page: FC = () => { {/* grow={false} fixes IE11 issue with nested flex */} - + @@ -81,7 +84,10 @@ export const Page: FC = () => { - +