[ML] Populate date fields for Transform (#108804)

* [ML] Add index pattern info & select control for date time

* [ML] Update translations

* [ML] Gracefully handle when index pattern is not available

* [ML] Fix import

* [ML] Handle when unmounted

* [ML] Remove load index patterns because we don't really need it

* [ML] Add error obj to error toasts

* [ML] Update tests

* [ML] Update hook

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen 2021-08-31 13:08:17 -05:00 committed by GitHub
parent bbfad19051
commit 42acf39a70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 172 additions and 40 deletions

View file

@ -42,8 +42,8 @@ export const useCloneAction = (forceDisable: boolean, transformNodes: number) =>
toastNotifications.addDanger(
i18n.translate('xpack.transform.clone.noIndexPatternErrorPromptText', {
defaultMessage:
'Unable to clone the transform . No index pattern exists for {indexPattern}.',
values: { indexPattern: indexPatternTitle },
'Unable to clone the transform {transformId}. No index pattern exists for {indexPattern}.',
values: { indexPattern: indexPatternTitle, transformId: item.id },
})
);
} else {
@ -52,11 +52,11 @@ export const useCloneAction = (forceDisable: boolean, transformNodes: number) =>
);
}
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.transform.clone.errorPromptText', {
toastNotifications.addError(e, {
title: i18n.translate('xpack.transform.clone.errorPromptText', {
defaultMessage: 'An error occurred checking if source index pattern exists',
})
);
}),
});
}
},
[

View file

@ -5,25 +5,63 @@
* 2.0.
*/
import React, { useContext, useMemo, useState } from 'react';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import { TransformConfigUnion } from '../../../../../../common/types/transform';
import { i18n } from '@kbn/i18n';
import { TransformListAction, TransformListRow } from '../../../../common';
import { AuthorizationContext } from '../../../../lib/authorization';
import { editActionNameText, EditActionName } from './edit_action_name';
import { useSearchItems } from '../../../../hooks/use_search_items';
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
import { TransformConfigUnion } from '../../../../../../common/types/transform';
export const useEditAction = (forceDisable: boolean, transformNodes: number) => {
const { canCreateTransform } = useContext(AuthorizationContext).capabilities;
const [config, setConfig] = useState<TransformConfigUnion>();
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const [indexPatternId, setIndexPatternId] = useState<string | undefined>();
const closeFlyout = () => setIsFlyoutVisible(false);
const showFlyout = (newConfig: TransformConfigUnion) => {
setConfig(newConfig);
setIsFlyoutVisible(true);
};
const { getIndexPatternIdByTitle } = useSearchItems(undefined);
const toastNotifications = useToastNotifications();
const appDeps = useAppDependencies();
const indexPatterns = appDeps.data.indexPatterns;
const clickHandler = useCallback(
async (item: TransformListRow) => {
try {
const indexPatternTitle = Array.isArray(item.config.source.index)
? item.config.source.index.join(',')
: item.config.source.index;
const currentIndexPatternId = getIndexPatternIdByTitle(indexPatternTitle);
if (currentIndexPatternId === undefined) {
toastNotifications.addWarning(
i18n.translate('xpack.transform.edit.noIndexPatternErrorPromptText', {
defaultMessage:
'Unable to get index pattern the transform {transformId}. No index pattern exists for {indexPattern}.',
values: { indexPattern: indexPatternTitle, transformId: item.id },
})
);
}
setIndexPatternId(currentIndexPatternId);
setConfig(item.config);
setIsFlyoutVisible(true);
} catch (e) {
toastNotifications.addError(e, {
title: i18n.translate('xpack.transform.edit.errorPromptText', {
defaultMessage: 'An error occurred checking if source index pattern exists',
}),
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[indexPatterns, toastNotifications, getIndexPatternIdByTitle]
);
const action: TransformListAction = useMemo(
() => ({
@ -32,10 +70,10 @@ export const useEditAction = (forceDisable: boolean, transformNodes: number) =>
description: editActionNameText,
icon: 'pencil',
type: 'icon',
onClick: (item: TransformListRow) => showFlyout(item.config),
onClick: (item: TransformListRow) => clickHandler(item),
'data-test-subj': 'transformActionEdit',
}),
[canCreateTransform, forceDisable, transformNodes]
[canCreateTransform, clickHandler, forceDisable, transformNodes]
);
return {
@ -43,6 +81,6 @@ export const useEditAction = (forceDisable: boolean, transformNodes: number) =>
config,
closeFlyout,
isFlyoutVisible,
showFlyout,
indexPatternId,
};
};

View file

@ -30,7 +30,6 @@ import { getErrorMessage } from '../../../../../../common/utils/errors';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common';
import { useToastNotifications } from '../../../../app_dependencies';
import { useApi } from '../../../../hooks/use_api';
import { EditTransformFlyoutCallout } from './edit_transform_flyout_callout';
@ -43,9 +42,14 @@ import {
interface EditTransformFlyoutProps {
closeFlyout: () => void;
config: TransformConfigUnion;
indexPatternId?: string;
}
export const EditTransformFlyout: FC<EditTransformFlyoutProps> = ({ closeFlyout, config }) => {
export const EditTransformFlyout: FC<EditTransformFlyoutProps> = ({
closeFlyout,
config,
indexPatternId,
}) => {
const api = useApi();
const toastNotifications = useToastNotifications();
@ -96,7 +100,10 @@ export const EditTransformFlyout: FC<EditTransformFlyoutProps> = ({ closeFlyout,
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody banner={<EditTransformFlyoutCallout />}>
<EditTransformFlyoutForm editTransformFlyout={[state, dispatch]} />
<EditTransformFlyoutForm
editTransformFlyout={[state, dispatch]}
indexPatternId={indexPatternId}
/>
{errorMessage !== undefined && (
<>
<EuiSpacer size="m" />

View file

@ -5,23 +5,58 @@
* 2.0.
*/
import React, { FC } from 'react';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { EuiForm, EuiAccordion, EuiSpacer } from '@elastic/eui';
import { EuiForm, EuiAccordion, EuiSpacer, EuiSelect, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input';
import { UseEditTransformFlyoutReturnType } from './use_edit_transform_flyout';
import { useAppDependencies } from '../../../../app_dependencies';
import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common';
interface EditTransformFlyoutFormProps {
editTransformFlyout: UseEditTransformFlyoutReturnType;
indexPatternId?: string;
}
export const EditTransformFlyoutForm: FC<EditTransformFlyoutFormProps> = ({
editTransformFlyout: [state, dispatch],
indexPatternId,
}) => {
const formFields = state.formFields;
const [dateFieldNames, setDateFieldNames] = useState<string[]>([]);
const appDeps = useAppDependencies();
const indexPatternsClient = appDeps.data.indexPatterns;
useEffect(
function getDateFields() {
let unmounted = false;
if (indexPatternId !== undefined) {
indexPatternsClient.get(indexPatternId).then((indexPattern) => {
if (indexPattern) {
const dateTimeFields = indexPattern.fields
.filter((f) => f.type === KBN_FIELD_TYPES.DATE)
.map((f) => f.name)
.sort();
if (!unmounted) {
setDateFieldNames(dateTimeFields);
}
}
});
return () => {
unmounted = true;
};
}
},
[indexPatternId, indexPatternsClient]
);
const retentionDateFieldOptions = useMemo(() => {
return Array.isArray(dateFieldNames) ? dateFieldNames.map((text: string) => ({ text })) : [];
}, [dateFieldNames]);
return (
<EuiForm>
@ -112,19 +147,57 @@ export const EditTransformFlyoutForm: FC<EditTransformFlyoutFormProps> = ({
paddingSize="s"
>
<div data-test-subj="transformEditAccordionRetentionPolicyContent">
{' '}
<EditTransformFlyoutFormTextInput
dataTestSubj="transformEditFlyoutRetentionPolicyFieldInput"
errorMessages={formFields.retentionPolicyField.errorMessages}
label={i18n.translate(
'xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldLabel',
{
defaultMessage: 'Field',
}
)}
onChange={(value) => dispatch({ field: 'retentionPolicyField', value })}
value={formFields.retentionPolicyField.value}
/>
{
// If index pattern or date fields info not available
// gracefully defaults to text input
indexPatternId ? (
<EuiFormRow
label={i18n.translate(
'xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldLabel',
{
defaultMessage: 'Field',
}
)}
isInvalid={formFields.retentionPolicyField.errorMessages.length > 0}
error={formFields.retentionPolicyField.errorMessages}
helpText={i18n.translate(
'xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText',
{
defaultMessage:
'Select the date field that can be used to identify out of date documents in the destination index.',
}
)}
>
<EuiSelect
aria-label={i18n.translate(
'xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldSelectAriaLabel',
{
defaultMessage: 'Date field to set retention policy',
}
)}
data-test-subj="transformEditFlyoutRetentionPolicyFieldSelect"
options={retentionDateFieldOptions}
value={formFields.retentionPolicyField.value}
onChange={(e) =>
dispatch({ field: 'retentionPolicyField', value: e.target.value })
}
/>
</EuiFormRow>
) : (
<EditTransformFlyoutFormTextInput
dataTestSubj="transformEditFlyoutRetentionPolicyFieldInput"
errorMessages={formFields.retentionPolicyField.errorMessages}
label={i18n.translate(
'xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldLabel',
{
defaultMessage: 'Field',
}
)}
onChange={(value) => dispatch({ field: 'retentionPolicyField', value })}
value={formFields.retentionPolicyField.value}
/>
)
}
<EditTransformFlyoutFormTextInput
dataTestSubj="transformEditFlyoutRetentionPolicyMaxAgeInput"
errorMessages={formFields.retentionPolicyMaxAge.errorMessages}

View file

@ -41,7 +41,11 @@ export const useActions = ({
<>
{startAction.isModalVisible && <StartActionModal {...startAction} />}
{editAction.config && editAction.isFlyoutVisible && (
<EditTransformFlyout closeFlyout={editAction.closeFlyout} config={editAction.config} />
<EditTransformFlyout
closeFlyout={editAction.closeFlyout}
config={editAction.config}
indexPatternId={editAction.indexPatternId}
/>
)}
{deleteAction.isModalVisible && <DeleteActionModal {...deleteAction} />}
</>

View file

@ -23566,7 +23566,6 @@
"xpack.transform.clone.errorPromptText": "ソースインデックスパターンが存在するかどうかを確認するときにエラーが発生しました",
"xpack.transform.clone.errorPromptTitle": "変換構成の取得中にエラーが発生しました。",
"xpack.transform.clone.fetchErrorPromptText": "KibanaインデックスパターンIDを取得できませんでした。",
"xpack.transform.clone.noIndexPatternErrorPromptText": "変換を複製できません。{indexPattern}のインデックスパターンが存在しません。",
"xpack.transform.cloneTransform.breadcrumbTitle": "クローン変換",
"xpack.transform.createTransform.breadcrumbTitle": "変換の作成",
"xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "ディスティネーションインデックス{destinationIndex}の削除中にエラーが発生しました",

View file

@ -24117,7 +24117,6 @@
"xpack.transform.clone.errorPromptText": "检查源索引模式是否存在时发生错误",
"xpack.transform.clone.errorPromptTitle": "获取转换配置时发生错误。",
"xpack.transform.clone.fetchErrorPromptText": "无法提取 Kibana 索引模式 ID。",
"xpack.transform.clone.noIndexPatternErrorPromptText": "无法克隆转换。对于 {indexPattern},不存在索引模式。",
"xpack.transform.cloneTransform.breadcrumbTitle": "克隆转换",
"xpack.transform.createTransform.breadcrumbTitle": "创建转换",
"xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "删除目标索引 {destinationIndex} 时发生错误",

View file

@ -158,10 +158,7 @@ export default function ({ getService }: FtrProviderContext) {
'should have the retention policy inputs enabled'
);
await transform.editFlyout.openTransformEditAccordionRetentionPolicySettings();
await transform.editFlyout.assertTransformEditFlyoutInputEnabled(
'RetentionPolicyField',
true
);
await transform.editFlyout.assertTransformEditFlyoutRetentionPolicySelectEnabled(true);
await transform.editFlyout.assertTransformEditFlyoutInputEnabled(
'RetentionPolicyMaxAge',
true

View file

@ -37,6 +37,21 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext)
);
},
async assertTransformEditFlyoutRetentionPolicySelectEnabled(expectedValue: boolean) {
await testSubjects.existOrFail(`transformEditFlyoutRetentionPolicyFieldSelect`, {
timeout: 1000,
});
const isEnabled = await testSubjects.isEnabled(
`transformEditFlyoutRetentionPolicyFieldSelect`
);
expect(isEnabled).to.eql(
expectedValue,
`Expected 'transformEditFlyoutRetentionPolicyFieldSelect' input to be '${
expectedValue ? 'enabled' : 'disabled'
}' (got '${isEnabled ? 'enabled' : 'disabled'}')`
);
},
async assertTransformEditFlyoutInputEnabled(input: string, expectedValue: boolean) {
await testSubjects.existOrFail(`transformEditFlyout${input}Input`, { timeout: 1000 });
const isEnabled = await testSubjects.isEnabled(`transformEditFlyout${input}Input`);