Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] DF Analytics: add ability to edit job for fields supported by API #70489

Merged
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) {
peteharverson marked this conversation as resolved.
Show resolved Hide resolved
setMmlValidationError(
i18n.translate('xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError', {
defaultMessage: 'Model memory limit data unit unrecognized. It must be {str}',
qn895 marked this conversation as resolved.
Show resolved Hide resolved
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