Skip to content

Commit

Permalink
[ML] DF Analytics: add ability to edit job for fields supported by API (
Browse files Browse the repository at this point in the history
#70489)

* wip: add edit action to dfanalytics table

* add update endpoint and edit flyout

* show success and error toasts. close flyout and refresh on success

* show permission message in edit action

* update types

* disable update button if mml not valid

* show error in toast, init values are config values

* fix undefined check for allow lazy start

* prevent update if mml is empty
  • Loading branch information
alvarezmelissa87 authored Jul 6, 2020
1 parent 321fb87 commit a4340f0
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 2 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/ml/common/util/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export function requiredValidator() {

export type ValidationResult = object | null;

export type MemoryInputValidatorResult = { invalidUnits: { allowedUnits: string } } | null;

export function memoryInputValidator(allowedUnits = ALLOWED_DATA_UNITS) {
return (value: any) => {
if (typeof value !== 'string' || value === '') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,14 @@ export const isClassificationEvaluateResponse = (
);
};

export interface UpdateDataFrameAnalyticsConfig {
allow_lazy_start?: string;
description?: string;
model_memory_limit?: string;
}

export interface DataFrameAnalyticsConfig {
id: DataFrameAnalyticsId;
// Description attribute is not supported yet
description?: string;
dest: {
index: IndexName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
useRefreshAnalyticsList,
DataFrameAnalyticsId,
DataFrameAnalyticsConfig,
UpdateDataFrameAnalyticsConfig,
IndexName,
IndexPattern,
REFRESH_ANALYTICS_LIST_STATE,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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, { useState, FC } from 'react';

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

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

import { checkPermission } from '../../../../../capabilities/check_capabilities';
import { DataFrameAnalyticsListRow } from './common';

import { EditAnalyticsFlyout } from './edit_analytics_flyout';

interface EditActionProps {
item: DataFrameAnalyticsListRow;
}

export const EditAction: FC<EditActionProps> = ({ item }) => {
const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics');

const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const closeFlyout = () => setIsFlyoutVisible(false);
const showFlyout = () => setIsFlyoutVisible(true);

const buttonEditText = i18n.translate('xpack.ml.dataframe.analyticsList.editActionName', {
defaultMessage: 'Edit',
});

const editButton = (
<EuiButtonEmpty
data-test-subj="mlAnalyticsJobEditButton"
size="xs"
color="text"
disabled={!canCreateDataFrameAnalytics}
iconType="copy"
onClick={showFlyout}
aria-label={buttonEditText}
>
{buttonEditText}
</EuiButtonEmpty>
);

if (!canCreateDataFrameAnalytics) {
return (
<EuiToolTip
position="top"
content={i18n.translate('xpack.ml.dataframe.analyticsList.editActionPermissionTooltip', {
defaultMessage: 'You do not have permission to edit analytics jobs.',
})}
>
{editButton}
</EuiToolTip>
);
}

return (
<>
{editButton}
{isFlyoutVisible && <EditAnalyticsFlyout closeFlyout={closeFlyout} item={item} />}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow }
import { stopAnalytics } from '../../services/analytics_service';

import { StartAction } from './action_start';
import { EditAction } from './action_edit';
import { DeleteAction } from './action_delete';

interface Props {
Expand Down Expand Up @@ -133,6 +134,11 @@ export const getActions = (
return stopButton;
},
},
{
render: (item: DataFrameAnalyticsListRow) => {
return <EditAction item={item} />;
},
},
{
render: (item: DataFrameAnalyticsListRow) => {
return <DeleteAction item={item} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*
* 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 {
EuiButton,
EuiButtonEmpty,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiForm,
EuiFormRow,
EuiOverlayMask,
EuiSelect,
EuiTitle,
} from '@elastic/eui';

import { useMlKibana } from '../../../../../contexts/kibana';
import { ml } from '../../../../../services/ml_api_service';
import {
memoryInputValidator,
MemoryInputValidatorResult,
} from '../../../../../../../common/util/validators';
import { extractErrorMessage } from '../../../../../../../common/util/errors';
import { DataFrameAnalyticsListRow, DATA_FRAME_TASK_STATE } from './common';
import {
useRefreshAnalyticsList,
UpdateDataFrameAnalyticsConfig,
} from '../../../../common/analytics';

interface EditAnalyticsJobFlyoutProps {
closeFlyout: () => void;
item: DataFrameAnalyticsListRow;
}

let mmLValidator: (value: any) => MemoryInputValidatorResult;

export const EditAnalyticsFlyout: FC<EditAnalyticsJobFlyoutProps> = ({ closeFlyout, item }) => {
const { id: jobId, config } = item;
const { state } = item.stats;
const initialAllowLazyStart =
config.allow_lazy_start !== undefined ? String(config.allow_lazy_start) : '';

const [allowLazyStart, setAllowLazyStart] = useState<string>(initialAllowLazyStart);
const [description, setDescription] = useState<string>(config.description || '');
const [modelMemoryLimit, setModelMemoryLimit] = useState<string>(config.model_memory_limit);
const [mmlValidationError, setMmlValidationError] = useState<string | undefined>();

const {
services: { notifications },
} = useMlKibana();
const { refresh } = useRefreshAnalyticsList();

// Disable if mml is not valid
const updateButtonDisabled = mmlValidationError !== undefined;

useEffect(() => {
if (mmLValidator === undefined) {
mmLValidator = memoryInputValidator();
}
// validate mml and create validation message
if (modelMemoryLimit !== '') {
const validationResult = mmLValidator(modelMemoryLimit);
if (validationResult !== null && validationResult.invalidUnits) {
setMmlValidationError(
i18n.translate('xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError', {
defaultMessage: 'Model memory limit data unit unrecognized. It must be {str}',
values: { str: validationResult.invalidUnits.allowedUnits },
})
);
} else {
setMmlValidationError(undefined);
}
} else {
setMmlValidationError(
i18n.translate('xpack.ml.dataframe.analytics.create.modelMemoryEmptyError', {
defaultMessage: 'Model memory limit must not be empty',
})
);
}
}, [modelMemoryLimit]);

const onSubmit = async () => {
const updateConfig: UpdateDataFrameAnalyticsConfig = Object.assign(
{
allow_lazy_start: allowLazyStart,
description,
},
modelMemoryLimit && { model_memory_limit: modelMemoryLimit }
);

try {
await ml.dataFrameAnalytics.updateDataFrameAnalytics(jobId, updateConfig);
notifications.toasts.addSuccess(
i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutSuccessMessage', {
defaultMessage: 'Analytics job {jobId} has been updated.',
values: { jobId },
})
);
refresh();
closeFlyout();
} catch (e) {
// eslint-disable-next-line
console.error(e);

notifications.toasts.addDanger({
title: i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutErrorMessage', {
defaultMessage: 'Could not save changes to analytics job {jobId}',
values: {
jobId,
},
}),
text: extractErrorMessage(e),
});
}
};

return (
<EuiOverlayMask>
<EuiFlyout
onClose={closeFlyout}
hideCloseButton
aria-labelledby="analyticsEditFlyoutTitle"
data-test-subj="analyticsEditFlyout"
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id="analyticsEditFlyoutTitle">
{i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutTitle', {
defaultMessage: 'Edit {jobId}',
values: {
jobId,
},
})}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiForm>
<EuiFormRow
label={i18n.translate(
'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartLabel',
{
defaultMessage: 'Allow lazy start',
}
)}
>
<EuiSelect
aria-label={i18n.translate(
'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartAriaLabel',
{
defaultMessage: 'Update allow lazy start.',
}
)}
data-test-subj="mlAnalyticsEditFlyoutAllowLazyStartInput"
options={[
{
value: 'true',
text: i18n.translate(
'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartTrueValue',
{
defaultMessage: 'True',
}
),
},
{
value: 'false',
text: i18n.translate(
'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartFalseValue',
{
defaultMessage: 'False',
}
),
},
]}
value={allowLazyStart}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setAllowLazyStart(e.target.value)
}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate(
'xpack.ml.dataframe.analyticsList.editFlyout.descriptionLabel',
{
defaultMessage: 'Description',
}
)}
>
<EuiFieldText
data-test-subj="mlAnalyticsEditFlyoutDescriptionInput"
value={description}
onChange={(e) => setDescription(e.target.value)}
aria-label={i18n.translate(
'xpack.ml.dataframe.analyticsList.editFlyout.descriptionAriaLabel',
{
defaultMessage: 'Update the job description.',
}
)}
/>
</EuiFormRow>
<EuiFormRow
helpText={
state !== DATA_FRAME_TASK_STATE.STOPPED &&
i18n.translate('xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryHelpText', {
defaultMessage: 'Model memory limit cannot be edited while the job is running.',
})
}
label={i18n.translate(
'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitLabel',
{
defaultMessage: 'Model memory limit',
}
)}
isInvalid={mmlValidationError !== undefined}
error={mmlValidationError}
>
<EuiFieldText
data-test-subj="mlAnalyticsEditFlyoutmodelMemoryLimitInput"
isInvalid={mmlValidationError !== undefined}
readOnly={state !== DATA_FRAME_TASK_STATE.STOPPED}
value={modelMemoryLimit}
onChange={(e) => setModelMemoryLimit(e.target.value)}
aria-label={i18n.translate(
'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitAriaLabel',
{
defaultMessage: 'Update the model memory limit.',
}
)}
/>
</EuiFormRow>
</EuiForm>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
{i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutCancelButtonText', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="analyticsEditFlyoutUpdateButton"
onClick={onSubmit}
fill
isDisabled={updateButtonDisabled}
>
{i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText', {
defaultMessage: 'Update',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
</EuiOverlayMask>
);
};
Loading

0 comments on commit a4340f0

Please sign in to comment.