Skip to content

Commit

Permalink
[ML] Data Frames: Analytics job creation. (elastic#43102)
Browse files Browse the repository at this point in the history
Introduces data frame analytics job creation. This first version uses a form within a modal on the analytics jobs list page, because there is no preview available and the form isn't very complex. Job creation might move to a separate page in the future.
  • Loading branch information
walterra committed Aug 13, 2019
1 parent 24d25a7 commit fbcca60
Show file tree
Hide file tree
Showing 8 changed files with 881 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC } from 'react';
import React, { Fragment, FC } from 'react';

import { EuiButton, EuiToolTip } from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';

import {
checkPermission,
createPermissionFailureMessage,
} from '../../../../../privilege/check_privilege';
import { createPermissionFailureMessage } from '../../../../../privilege/check_privilege';

import { moveToAnalyticsWizard } from '../../../../common';
import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form';

import { CreateAnalyticsForm } from '../create_analytics_form';
import { CreateAnalyticsModal } from '../create_analytics_modal';

export const CreateAnalyticsButton: FC = () => {
const disabled =
!checkPermission('canCreateDataFrameAnalytics') ||
!checkPermission('canStartStopDataFrameAnalytics');
const { state, actions } = useCreateAnalyticsForm();
const { disabled, isModalVisible } = state;
const { openModal } = actions;

const button = (
<EuiButton
disabled={true}
disabled={disabled}
fill
onClick={moveToAnalyticsWizard}
onClick={openModal}
iconType="plusInCircle"
size="s"
data-test-subj="mlDataFrameAnalyticsButtonCreate"
>
<FormattedMessage
id="xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton"
defaultMessage="Create data frame analytics job"
/>
{i18n.translate('xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton', {
defaultMessage: 'Create data frame analytics job',
})}
</EuiButton>
);

Expand All @@ -49,5 +48,14 @@ export const CreateAnalyticsButton: FC = () => {
);
}

return button;
return (
<Fragment>
{button}
{isModalVisible && (
<CreateAnalyticsModal actions={actions} formState={state}>
<CreateAnalyticsForm actions={actions} formState={state} />
</CreateAnalyticsModal>
)}
</Fragment>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
/*
* 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, { Fragment, FC } from 'react';

import {
EuiCallOut,
EuiComboBox,
EuiForm,
EuiFieldText,
EuiFormRow,
EuiLink,
EuiSpacer,
EuiSwitch,
} from '@elastic/eui';

import { i18n } from '@kbn/i18n';

import { metadata } from 'ui/metadata';

import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';

export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, formState }) => {
const { setFormState } = actions;
const {
createIndexPattern,
destinationIndex,
destinationIndexNameEmpty,
destinationIndexNameExists,
destinationIndexNameValid,
destinationIndexPatternTitleExists,
indexPatternsWithNumericFields,
indexPatternTitles,
isJobCreated,
jobId,
jobIdEmpty,
jobIdExists,
jobIdValid,
requestMessages,
sourceIndex,
sourceIndexNameEmpty,
sourceIndexNameExists,
sourceIndexNameValid,
} = formState;

return (
<EuiForm>
{requestMessages.map((requestMessage, i) => (
<Fragment key={i}>
<EuiCallOut
title={requestMessage.message}
color={requestMessage.error !== undefined ? 'danger' : 'primary'}
iconType={requestMessage.error !== undefined ? 'alert' : 'checkInCircleFilled'}
size="s"
>
{requestMessage.error !== undefined ? <p>{requestMessage.error}</p> : null}
</EuiCallOut>
<EuiSpacer size="s" />
</Fragment>
))}
{!isJobCreated && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.jobIdLabel', {
defaultMessage: 'Analytics job id',
})}
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists}
error={[
...(!jobIdEmpty && !jobIdValid
? [
i18n.translate('xpack.ml.dataframe.analytics.create.jobIdInvalidError', {
defaultMessage:
'Must contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and underscores only and must start and end with alphanumeric characters.',
}),
]
: []),
...(jobIdExists
? [
i18n.translate('xpack.ml.dataframe.analytics.create.jobIdExistsError', {
defaultMessage: 'An analytics job with this id already exists.',
}),
]
: []),
]}
>
<EuiFieldText
disabled={isJobCreated}
placeholder="analytics job id"
value={jobId}
onChange={e => setFormState({ jobId: e.target.value })}
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.jobIdInputAriaLabel',
{
defaultMessage: 'Choose a unique analytics job id.',
}
)}
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists}
/>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.sourceIndexLabel', {
defaultMessage: 'Source index',
})}
helpText={
!sourceIndexNameEmpty &&
!indexPatternsWithNumericFields.includes(sourceIndex) &&
i18n.translate('xpack.ml.dataframe.stepDetailsForm.sourceIndexHelpText', {
defaultMessage:
'This index pattern does not contain any numeric type fields. The analytics job may not be able to come up with any outliers.',
})
}
isInvalid={!sourceIndexNameEmpty && (!sourceIndexNameValid || !sourceIndexNameExists)}
error={
(!sourceIndexNameEmpty &&
!sourceIndexNameValid && [
<Fragment>
{i18n.translate('xpack.ml.dataframe.analytics.create.sourceIndexInvalidError', {
defaultMessage: 'Invalid source index name.',
})}
<br />
<EuiLink
href={`https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/indices-create-index.html#indices-create-index`}
target="_blank"
>
{i18n.translate(
'xpack.ml.dataframe.stepDetailsForm.sourceIndexInvalidErrorLink',
{
defaultMessage: 'Learn more about index name limitations.',
}
)}
</EuiLink>
</Fragment>,
]) ||
(!sourceIndexNameEmpty &&
!sourceIndexNameExists && [
<Fragment>
{i18n.translate(
'xpack.ml.dataframe.analytics.create.sourceIndexDoesNotExistError',
{
defaultMessage: 'An index with this name does not exist.',
}
)}
</Fragment>,
])
}
>
<Fragment>
{!isJobCreated && (
<EuiComboBox
placeholder={i18n.translate(
'xpack.ml.dataframe.analytics.create.sourceIndexPlaceholder',
{
defaultMessage: 'Choose a source index pattern or saved search.',
}
)}
singleSelection={{ asPlainText: true }}
options={indexPatternTitles.sort().map(d => ({ label: d }))}
selectedOptions={[{ label: sourceIndex }]}
onChange={selectedOptions =>
setFormState({ sourceIndex: selectedOptions[0].label || '' })
}
isClearable={false}
/>
)}
{isJobCreated && (
<EuiFieldText
disabled={true}
value={sourceIndex}
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.sourceIndexInputAriaLabel',
{
defaultMessage: 'Source index pattern or search.',
}
)}
/>
)}
</Fragment>
</EuiFormRow>

<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.destinationIndexLabel', {
defaultMessage: 'Destination index',
})}
isInvalid={!destinationIndexNameEmpty && !destinationIndexNameValid}
helpText={
destinationIndexNameExists &&
i18n.translate('xpack.ml.dataframe.analytics.create.destinationIndexHelpText', {
defaultMessage:
'An index with this name already exists. Be aware that running this analytics job will modify this destination index.',
})
}
error={
!destinationIndexNameEmpty &&
!destinationIndexNameValid && [
<Fragment>
{i18n.translate(
'xpack.ml.dataframe.analytics.create.destinationIndexInvalidError',
{
defaultMessage: 'Invalid destination index name.',
}
)}
<br />
<EuiLink
href={`https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/indices-create-index.html#indices-create-index`}
target="_blank"
>
{i18n.translate(
'xpack.ml.dataframe.stepDetailsForm.destinationIndexInvalidErrorLink',
{
defaultMessage: 'Learn more about index name limitations.',
}
)}
</EuiLink>
</Fragment>,
]
}
>
<EuiFieldText
disabled={isJobCreated}
placeholder="destination index"
value={destinationIndex}
onChange={e => setFormState({ destinationIndex: e.target.value })}
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.destinationIndexInputAriaLabel',
{
defaultMessage: 'Choose a unique destination index name.',
}
)}
isInvalid={!destinationIndexNameEmpty && !destinationIndexNameValid}
/>
</EuiFormRow>

<EuiFormRow
isInvalid={createIndexPattern && destinationIndexPatternTitleExists}
error={
createIndexPattern &&
destinationIndexPatternTitleExists && [
i18n.translate('xpack.ml.dataframe.analytics.create.indexPatternTitleError', {
defaultMessage: 'An index pattern with this title already exists.',
}),
]
}
>
<EuiSwitch
disabled={isJobCreated}
name="mlDataFrameAnalyticsCreateIndexPattern"
label={i18n.translate('xpack.ml.dataframe.analytics.create.createIndexPatternLabel', {
defaultMessage: 'Create index pattern',
})}
checked={createIndexPattern === true}
onChange={() => setFormState({ createIndexPattern: !createIndexPattern })}
/>
</EuiFormRow>
</Fragment>
)}
</EuiForm>
);
};
Original file line number Diff line number Diff line change
@@ -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 { CreateAnalyticsForm } from './create_analytics_form';
Loading

0 comments on commit fbcca60

Please sign in to comment.