[ML] Transforms: Adds per-transform setting for num_failure_retries to creation wizard and edit flyout and authorization info (#135486)

* [ML] Add optional num_failure_retries setting in creation wizard and edit flyout

* [ML] Fix logic for input validation

* Fix types & i18n

* Change to integerRangeMinus1To100

* Fix clone

* Fix test

* Update text

* [ML] Add functional tests

* [ML] Add functional tests for editting

* [ML] Update translations

* [ML] Surface num failure retries to stats

* [ML] Add authorization info

* [ML] Fix extra period

* [ML] Move numberValidator to its own package

* [ML] Add tests for cloning

* [ML] Update logic + add unit tests

* [ML] Fix expected value
This commit is contained in:
Quynh Nguyen 2022-07-08 09:57:59 -05:00 committed by GitHub
parent dd7d92f282
commit 6b51010141
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 461 additions and 92 deletions

View file

@ -8,3 +8,5 @@
export { buildSamplerAggregation } from './build_sampler_aggregation';
export { getAggIntervals } from './get_agg_intervals';
export { getSamplerAggregationsResponsePath } from './get_sampler_aggregations_response_path';
export type { NumberValidationResult } from './validate_number';
export { numberValidator } from './validate_number';

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { numberValidator } from '@kbn/ml-agg-utils';
describe('numberValidator', () => {
it('should only allow integers above zero', () => {
const integerOnlyValidator = numberValidator({ min: 1, integerOnly: true });
// invalid
expect(integerOnlyValidator(0)).toMatchObject({ min: true });
expect(integerOnlyValidator(0.1)).toMatchObject({ integerOnly: true });
// valid
expect(integerOnlyValidator(1)).toStrictEqual(null);
expect(integerOnlyValidator(100)).toStrictEqual(null);
});
it('should not allow value greater than max', () => {
const integerOnlyValidator = numberValidator({ min: 1, max: 8, integerOnly: true });
// invalid
expect(integerOnlyValidator(10)).toMatchObject({ max: true });
expect(integerOnlyValidator(11.1)).toMatchObject({ integerOnly: true, max: true });
// valid
expect(integerOnlyValidator(6)).toStrictEqual(null);
});
it('should allow non-integers', () => {
const integerOnlyValidator = numberValidator({ min: 1, max: 8, integerOnly: false });
// invalid
expect(integerOnlyValidator(10)).toMatchObject({ max: true });
expect(integerOnlyValidator(11.1)).toMatchObject({ max: true });
// valid
expect(integerOnlyValidator(6)).toStrictEqual(null);
expect(integerOnlyValidator(6.6)).toStrictEqual(null);
});
});

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
export interface NumberValidationResult {
min: boolean;
max: boolean;
integerOnly: boolean;
}
/**
* Validate if a number is greater than a specified minimum & lesser than specified maximum
*/
export function numberValidator(conditions?: {
min?: number;
max?: number;
integerOnly?: boolean;
}) {
if (
conditions?.min !== undefined &&
conditions.max !== undefined &&
conditions.min > conditions.max
) {
throw new Error('Invalid validator conditions');
}
return (value: number): NumberValidationResult | null => {
const result = {} as NumberValidationResult;
if (conditions?.min !== undefined && value < conditions.min) {
result.min = true;
}
if (conditions?.max !== undefined && value > conditions.max) {
result.max = true;
}
if (!!conditions?.integerOnly && !Number.isInteger(value)) {
result.integerOnly = true;
}
if (isPopulatedObject(result)) {
return result;
}
return null;
};
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { ALLOWED_DATA_UNITS } from '../constants/validation';
import { parseInterval } from './parse_interval';
@ -100,40 +99,3 @@ export function timeIntervalInputValidator() {
return null;
};
}
export interface NumberValidationResult {
min: boolean;
max: boolean;
integerOnly: boolean;
}
export function numberValidator(conditions?: {
min?: number;
max?: number;
integerOnly?: boolean;
}) {
if (
conditions?.min !== undefined &&
conditions.max !== undefined &&
conditions.min > conditions.max
) {
throw new Error('Invalid validator conditions');
}
return (value: number): NumberValidationResult | null => {
const result = {} as NumberValidationResult;
if (conditions?.min !== undefined && value < conditions.min) {
result.min = true;
}
if (conditions?.max !== undefined && value > conditions.max) {
result.max = true;
}
if (!!conditions?.integerOnly && !Number.isInteger(value)) {
result.integerOnly = true;
}
if (isPopulatedObject(result)) {
return result;
}
return null;
};
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { numberValidator, timeIntervalInputValidator } from '../../common/util/validators';
import { numberValidator } from '@kbn/ml-agg-utils';
import { timeIntervalInputValidator } from '../../common/util/validators';
export const validateLookbackInterval = timeIntervalInputValidator();
export const validateTopNBucket = numberValidator({ min: 1 });

View file

@ -32,12 +32,9 @@ import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import type { Observable } from 'rxjs';
import type { CoreTheme, OverlayStart } from '@kbn/core/public';
import { css } from '@emotion/react';
import { numberValidator } from '@kbn/ml-agg-utils';
import { isCloud } from '../../services/ml_server_info';
import {
composeValidators,
numberValidator,
requiredValidator,
} from '../../../../common/util/validators';
import { composeValidators, requiredValidator } from '../../../../common/util/validators';
interface StartDeploymentSetup {
config: ThreadingParams;

View file

@ -70,6 +70,8 @@ export const settingsSchema = schema.object({
max_page_search_size: schema.maybe(schema.nullable(schema.number())),
// The default value is null, which disables throttling.
docs_per_second: schema.maybe(schema.nullable(schema.number())),
// Optional value that takes precedence over cluster's setting.
num_failure_retries: schema.maybe(schema.nullable(schema.number())),
});
export const sourceSchema = schema.object({

View file

@ -25,6 +25,7 @@ export type TransformBaseConfig = PutTransformsRequestSchema & {
version?: string;
alerting_rules?: TransformHealthAlertRule[];
_meta?: Record<string, unknown>;
authorization?: object;
};
export interface PivotConfigDefinition {

View file

@ -311,6 +311,7 @@ describe('Transform: Common', () => {
transformFrequency: '10m',
transformSettingsMaxPageSearchSize: 100,
transformSettingsDocsPerSecond: 400,
transformSettingsNumFailureRetries: 5,
destinationIndex: 'the-destination-index',
destinationIngestPipeline: 'the-destination-ingest-pipeline',
touched: true,
@ -334,6 +335,7 @@ describe('Transform: Common', () => {
settings: {
max_page_search_size: 100,
docs_per_second: 400,
num_failure_retries: 5,
},
source: {
index: ['the-data-view-title'],

View file

@ -206,6 +206,9 @@ export const getCreateTransformSettingsRequestBody = (
DEFAULT_TRANSFORM_SETTINGS_DOCS_PER_SECOND
? { docs_per_second: transformDetailsState.transformSettingsDocsPerSecond }
: {}),
...(typeof transformDetailsState.transformSettingsNumFailureRetries === 'number'
? { num_failure_retries: transformDetailsState.transformSettingsNumFailureRetries }
: {}),
};
return Object.keys(settings).length > 0 ? { settings } : {};
};

View file

@ -33,6 +33,7 @@ export interface StepDetailsExposedState {
transformFrequency: string;
transformSettingsMaxPageSearchSize: number;
transformSettingsDocsPerSecond: number | null;
transformSettingsNumFailureRetries?: number;
valid: boolean;
dataViewTimeField?: string | undefined;
_meta?: Record<string, unknown>;
@ -52,6 +53,7 @@ export function getDefaultStepDetailsState(): StepDetailsExposedState {
transformFrequency: DEFAULT_TRANSFORM_FREQUENCY,
transformSettingsMaxPageSearchSize: DEFAULT_TRANSFORM_SETTINGS_MAX_PAGE_SEARCH_SIZE,
transformSettingsDocsPerSecond: DEFAULT_TRANSFORM_SETTINGS_DOCS_PER_SECOND,
transformSettingsNumFailureRetries: undefined,
destinationIndex: '',
destinationIngestPipeline: '',
touched: false,
@ -107,6 +109,9 @@ export function applyTransformConfigToDetailsState(
} else {
state.transformSettingsDocsPerSecond = null;
}
if (typeof transformConfig.settings?.num_failure_retries === 'number') {
state.transformSettingsNumFailureRetries = transformConfig.settings.num_failure_retries;
}
}
if (transformConfig._meta) {

View file

@ -27,6 +27,7 @@ import {
import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { integerRangeMinus1To100Validator } from '../../../transform_management/components/edit_transform_flyout/use_edit_transform_flyout';
import {
isEsIndices,
isEsIngestPipelines,
@ -304,6 +305,14 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
transformSettingsMaxPageSearchSize
);
const [transformSettingsNumFailureRetries, setTransformSettingsNumFailureRetries] = useState<
string | number | undefined
>(defaults.transformSettingsNumFailureRetries);
const isTransformSettingsNumFailureRetriesValid =
transformSettingsNumFailureRetries === undefined ||
transformSettingsNumFailureRetries === '-' ||
integerRangeMinus1To100Validator(transformSettingsNumFailureRetries).length === 0;
const valid =
!transformIdEmpty &&
transformIdValid &&
@ -336,6 +345,13 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
transformFrequency,
transformSettingsMaxPageSearchSize,
transformSettingsDocsPerSecond,
transformSettingsNumFailureRetries:
transformSettingsNumFailureRetries === undefined ||
transformSettingsNumFailureRetries === ''
? undefined
: typeof transformSettingsNumFailureRetries === 'number'
? transformSettingsNumFailureRetries
: parseInt(transformSettingsNumFailureRetries, 10),
destinationIndex,
destinationIngestPipeline,
touched: true,
@ -357,6 +373,7 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
transformDescription,
transformFrequency,
transformSettingsMaxPageSearchSize,
transformSettingsNumFailureRetries,
destinationIndex,
destinationIngestPipeline,
valid,
@ -840,6 +857,58 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
data-test-subj="transformMaxPageSearchSizeInput"
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="transformNumFailureRetriesFormRow"
label={i18n.translate(
'xpack.transform.stepDetailsForm.transformNumFailureRetriesLabel',
{
defaultMessage: 'Number of failure retries',
}
)}
isInvalid={!isTransformSettingsNumFailureRetriesValid}
error={
!isTransformSettingsNumFailureRetriesValid && [
i18n.translate('xpack.transform.stepDetailsForm.NumFailureRetriesError', {
defaultMessage:
'Number of retries needs to be between 0 and 100, or -1 for infinite retries.',
}),
]
}
helpText={i18n.translate(
'xpack.transform.stepDetailsForm.transformNumRetriesHelpText',
{
defaultMessage:
'The number of retries on a recoverable failure before the transform task is marked as failed. Set it to -1 for infinite retries.',
}
)}
>
<EuiFieldText
value={
transformSettingsNumFailureRetries ||
(transformSettingsNumFailureRetries !== undefined &&
transformSettingsNumFailureRetries >= -1)
? transformSettingsNumFailureRetries.toString()
: ''
}
onChange={(e) => {
if (e.target.value === '') {
setTransformSettingsNumFailureRetries(undefined);
return;
}
setTransformSettingsNumFailureRetries(
e.target.value === '-' ? '-' : parseInt(e.target.value, 10)
);
}}
aria-label={i18n.translate(
'xpack.transform.stepDetailsForm.numFailureRetriesAriaLabel',
{
defaultMessage: 'Choose a maximum number of retries.',
}
)}
isInvalid={!isTransformSettingsNumFailureRetriesValid}
data-test-subj="transformNumFailureRetriesInput"
/>
</EuiFormRow>
</EuiAccordion>
</EuiForm>
</div>

View file

@ -25,6 +25,7 @@ export const StepDetailsSummary: FC<StepDetailsExposedState> = React.memo((props
transformDescription,
transformFrequency,
transformSettingsMaxPageSearchSize,
transformSettingsNumFailureRetries,
destinationIndex,
destinationIngestPipeline,
touched,
@ -153,6 +154,16 @@ export const StepDetailsSummary: FC<StepDetailsExposedState> = React.memo((props
>
<span>{transformSettingsMaxPageSearchSize}</span>
</EuiFormRow>
{typeof transformSettingsNumFailureRetries === 'number' ? (
<EuiFormRow
data-test-subj={'transformWizardAdvancedSettingsNumFailureRetriesLabel'}
label={i18n.translate('xpack.transform.stepDetailsSummary.numFailureRetriesLabel', {
defaultMessage: 'Number of retries',
})}
>
<span>{transformSettingsNumFailureRetries}</span>
</EuiFormRow>
) : null}
</EuiAccordion>
</div>
);

View file

@ -323,7 +323,7 @@ export const EditTransformFlyoutForm: FC<EditTransformFlyoutFormProps> = ({
dataTestSubj="transformEditFlyoutDocsPerSecondInput"
errorMessages={formFields.docsPerSecond.errorMessages}
helpText={i18n.translate(
'xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext',
'xpack.transform.transformList.editFlyoutFormDocsPerSecondHelpText',
{
defaultMessage:
'To enable throttling, set a limit of documents to input per second.',
@ -343,7 +343,7 @@ export const EditTransformFlyoutForm: FC<EditTransformFlyoutFormProps> = ({
dataTestSubj="transformEditFlyoutMaxPageSearchSizeInput"
errorMessages={formFields.maxPageSearchSize.errorMessages}
helpText={i18n.translate(
'xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext',
'xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelpText',
{
defaultMessage:
'The initial page size to use for the composite aggregation for each checkpoint.',
@ -365,6 +365,22 @@ export const EditTransformFlyoutForm: FC<EditTransformFlyoutFormProps> = ({
}
)}
/>
<EditTransformFlyoutFormTextInput
dataTestSubj="transformEditFlyoutNumFailureRetriesInput"
errorMessages={formFields.numFailureRetries.errorMessages}
helpText={i18n.translate(
'xpack.transform.transformList.editFlyoutFormNumFailureRetriesHelpText',
{
defaultMessage:
'The number of retries on a recoverable failure before the transform task is marked as failed. Set it to -1 for infinite retries.',
}
)}
label={i18n.translate('xpack.transform.transformList.numFailureRetriesLabel', {
defaultMessage: 'Number of failure retries',
})}
onChange={(value) => dispatch({ field: 'numFailureRetries', value })}
value={formFields.numFailureRetries.value}
/>
</div>
</EuiAccordion>
</EuiForm>

View file

@ -7,6 +7,7 @@
import { isEqual } from 'lodash';
import { merge } from 'lodash';
import { numberValidator } from '@kbn/ml-agg-utils';
import { useReducer } from 'react';
@ -44,7 +45,8 @@ type EditTransformFormFields =
| 'docsPerSecond'
| 'maxPageSearchSize'
| 'retentionPolicyField'
| 'retentionPolicyMaxAge';
| 'retentionPolicyMaxAge'
| 'numFailureRetries';
type EditTransformFlyoutFieldsState = Record<EditTransformFormFields, FormField>;
@ -107,16 +109,30 @@ type Validator = (value: any, isOptional?: boolean) => string[];
// We do this so we have fine grained control over field validation and the option to
// cast to special values like `null` for disabling `docs_per_second`.
const numberAboveZeroNotValidErrorMessage = i18n.translate(
'xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage',
'xpack.transform.transformList.editFlyoutFormNumberAboveZeroNotValidErrorMessage',
{
defaultMessage: 'Value needs to be an integer above zero.',
}
);
const numberRangeMinus1To100NotValidErrorMessage = i18n.translate(
'xpack.transform.transformList.editFlyoutFormNumberGreaterThanOrEqualToNegativeOneNotValidErrorMessage',
{
defaultMessage: 'Number of retries needs to be between 0 and 100, or -1 for infinite retries.',
}
);
export const integerAboveZeroValidator: Validator = (value) =>
!isNaN(value) && Number.isInteger(+value) && +value > 0 && !(value + '').includes('.')
!(value + '').includes('.') && numberValidator({ min: 1, integerOnly: true })(+value) === null
? []
: [numberAboveZeroNotValidErrorMessage];
export const integerRangeMinus1To100Validator: Validator = (value) =>
!(value + '').includes('.') &&
numberValidator({ min: -1, max: 100, integerOnly: true })(+value) === null
? []
: [numberRangeMinus1To100NotValidErrorMessage];
const numberRange10To10000NotValidErrorMessage = i18n.translate(
'xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage',
{
@ -124,7 +140,8 @@ const numberRange10To10000NotValidErrorMessage = i18n.translate(
}
);
export const integerRange10To10000Validator: Validator = (value) =>
integerAboveZeroValidator(value).length === 0 && +value >= 10 && +value <= 10000
!(value + '').includes('.') &&
numberValidator({ min: 10, max: 100001, integerOnly: true })(+value) === null
? []
: [numberRange10To10000NotValidErrorMessage];
@ -214,6 +231,7 @@ const validate = {
string: stringValidator,
frequency: frequencyValidator,
integerAboveZero: integerAboveZeroValidator,
integerRangeMinus1To100: integerRangeMinus1To100Validator,
integerRange10To10000: integerRange10To10000Validator,
retentionPolicyMaxAge: retentionPolicyMaxAgeValidator,
} as const;
@ -407,6 +425,18 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo
valueParser: (v) => +v,
}
),
numFailureRetries: initializeField(
'numFailureRetries',
'settings.num_failure_retries',
config,
{
defaultValue: undefined,
isNullable: true,
isOptional: true,
validator: 'integerRangeMinus1To100',
valueParser: (v) => +v,
}
),
// retention_policy.*
retentionPolicyField: initializeField(

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { FC } from 'react';
import React, { FC, useMemo } from 'react';
import { EuiButtonEmpty, EuiTabbedContent } from '@elastic/eui';
import { Optional } from '@kbn/utility-types';
@ -13,6 +13,7 @@ import { i18n } from '@kbn/i18n';
import { stringHash } from '@kbn/ml-string-hash';
import moment from 'moment-timezone';
import { isDefined } from '../../../../../../common/types/common';
import { TransformListRow } from '../../../../common';
import { useAppDependencies } from '../../../../app_dependencies';
import { ExpandedRowDetailsPane, SectionConfig, SectionItem } from './expanded_row_details_pane';
@ -70,37 +71,52 @@ export const ExpandedRow: FC<Props> = ({ item, onAlertEdit }) => {
position: 'right',
};
const configItems: Item[] = [
{
title: 'transform_id',
description: item.id,
},
{
title: 'transform_version',
description: item.config.version,
},
{
title: 'description',
description: item.config.description ?? '',
},
{
title: 'create_time',
description:
formatHumanReadableDateTimeSeconds(moment(item.config.create_time).unix() * 1000) ?? '',
},
{
title: 'source_index',
description: Array.isArray(item.config.source.index)
? item.config.source.index[0]
: item.config.source.index,
},
{
title: 'destination_index',
description: Array.isArray(item.config.dest.index)
? item.config.dest.index[0]
: item.config.dest.index,
},
];
const configItems = useMemo(() => {
const configs: Item[] = [
{
title: 'transform_id',
description: item.id,
},
{
title: 'transform_version',
description: item.config.version,
},
{
title: 'description',
description: item.config.description ?? '',
},
{
title: 'create_time',
description:
formatHumanReadableDateTimeSeconds(moment(item.config.create_time).unix() * 1000) ?? '',
},
{
title: 'source_index',
description: Array.isArray(item.config.source.index)
? item.config.source.index[0]
: item.config.source.index,
},
{
title: 'destination_index',
description: Array.isArray(item.config.dest.index)
? item.config.dest.index[0]
: item.config.dest.index,
},
{
title: 'authorization',
description: item.config.authorization ? JSON.stringify(item.config.authorization) : '',
},
];
if (isDefined(item.config.settings?.num_failure_retries)) {
configs.push({
title: 'num_failure_retries',
description: item.config.settings?.num_failure_retries ?? '',
});
}
return configs;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item?.config]);
const general: SectionConfig = {
title: 'General',

View file

@ -30342,16 +30342,16 @@
"xpack.transform.transformList.editFlyoutFormDestinationButtonContent": "Configuration de destination",
"xpack.transform.transformList.editFlyoutFormDestinationIndexLabel": "Index de destination",
"xpack.transform.transformList.editFlyoutFormDestinationIngestPipelineLabel": "Pipeline d'ingestion",
"xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "Pour activer la régulation, définissez une limite de documents à saisir par seconde.",
"xpack.transform.transformList.editFlyoutFormDocsPerSecondHelpText": "Pour activer la régulation, définissez une limite de documents à saisir par seconde.",
"xpack.transform.transformList.editFlyoutFormDocsPerSecondLabel": "Documents par seconde",
"xpack.transform.transformList.editFlyoutFormFrequencyHelpText": "Intervalle entre les vérifications de modifications dans les index source lorsque la transformation est exécutée en continu. Détermine également l'intervalle des nouvelles tentatives en cas d'échecs temporaires lorsque la transformation effectue une recherche ou une indexation. La valeur minimale est de 1 s et la valeur maximale de 1 h.",
"xpack.transform.transformList.editFlyoutFormFrequencyLabel": "Fréquence",
"xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "La valeur de fréquence n'est pas valide.",
"xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText": "Par défaut : {defaultValue}",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext": "Définit la taille de pages initiale à utiliser pour l'agrégation imbriquée de chaque point de contrôle.",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelpText": "Définit la taille de pages initiale à utiliser pour l'agrégation imbriquée de chaque point de contrôle.",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeLabel": "Taille maximale de recherche de pages",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText": "Par défaut : {defaultValue}",
"xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "La valeur doit être un entier supérieur à zéro.",
"xpack.transform.transformList.editFlyoutFormNumberAboveZeroNotValidErrorMessage": "La valeur doit être un entier supérieur à zéro.",
"xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "La valeur doit être un entier compris entre 10 et 10 000.",
"xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "Champs requis.",
"xpack.transform.transformList.editFlyoutFormRetentionMaxAgeFieldLabel": "Âge maximal",

View file

@ -30326,16 +30326,16 @@
"xpack.transform.transformList.editFlyoutFormDestinationButtonContent": "ディスティネーション構成",
"xpack.transform.transformList.editFlyoutFormDestinationIndexLabel": "デスティネーションインデックス",
"xpack.transform.transformList.editFlyoutFormDestinationIngestPipelineLabel": "インジェストパイプライン",
"xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "スロットリングを有効にするには、毎秒入力するドキュメントの上限を設定します。",
"xpack.transform.transformList.editFlyoutFormDocsPerSecondHelpText": "スロットリングを有効にするには、毎秒入力するドキュメントの上限を設定します。",
"xpack.transform.transformList.editFlyoutFormDocsPerSecondLabel": "毎秒あたりのドキュメント",
"xpack.transform.transformList.editFlyoutFormFrequencyHelpText": "変換が連続実行されているときにソースインデックスで変更を確認する間の間隔。また、変換が検索またはインデックス中に一時障害が発生した場合に、再試行する間隔も決定します。最小値は1秒で、最大値は1時間です。",
"xpack.transform.transformList.editFlyoutFormFrequencyLabel": "頻度",
"xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "頻度値が無効です。",
"xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText": "デフォルト:{defaultValue}",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext": "各チェックポイントの複合集計で使用する、初期ページサイズを定義します。",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelpText": "各チェックポイントの複合集計で使用する、初期ページサイズを定義します。",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeLabel": "最大ページ検索サイズ",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText": "デフォルト:{defaultValue}",
"xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "値は1以上の整数でなければなりません。",
"xpack.transform.transformList.editFlyoutFormNumberAboveZeroNotValidErrorMessage": "値は1以上の整数でなければなりません。",
"xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "値は1010000の範囲の整数でなければなりません。",
"xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必須フィールド。",
"xpack.transform.transformList.editFlyoutFormRetentionMaxAgeFieldLabel": "最大年齢",

View file

@ -30354,16 +30354,16 @@
"xpack.transform.transformList.editFlyoutFormDestinationButtonContent": "目标配置",
"xpack.transform.transformList.editFlyoutFormDestinationIndexLabel": "目标索引",
"xpack.transform.transformList.editFlyoutFormDestinationIngestPipelineLabel": "采集管道",
"xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "要启用节流,请设置每秒要输入的文档限值。",
"xpack.transform.transformList.editFlyoutFormDocsPerSecondHelpText": "要启用节流,请设置每秒要输入的文档限值。",
"xpack.transform.transformList.editFlyoutFormDocsPerSecondLabel": "每秒文档数",
"xpack.transform.transformList.editFlyoutFormFrequencyHelpText": "在转换不间断地执行时检查源索引更改的时间间隔。还确定在转换搜索或索引时发生暂时失败时的重试时间间隔。最小值为 1 秒,最大值为 1 小时。",
"xpack.transform.transformList.editFlyoutFormFrequencyLabel": "频率",
"xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "频率值无效。",
"xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText": "默认值:{defaultValue}",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelptext": "定义用于每个检查点的组合聚合的初始页面大小。",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeHelpText": "定义用于每个检查点的组合聚合的初始页面大小。",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizeLabel": "最大页面搜索大小",
"xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText": "默认值:{defaultValue}",
"xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "值必须是大于零的整数。",
"xpack.transform.transformList.editFlyoutFormNumberAboveZeroNotValidErrorMessage": "值必须是大于零的整数。",
"xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "值必须是介于 10 到 10000 之间的整数。",
"xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必填字段。",
"xpack.transform.transformList.editFlyoutFormRetentionMaxAgeFieldLabel": "最大存在时间",

View file

@ -23,6 +23,11 @@ interface TestData {
expected: any;
}
function getNumFailureRetriesStr(value: number | null | undefined) {
if (value === null || value === undefined) return '';
return value.toString();
}
function getTransformConfig(): TransformPivotConfig {
const date = Date.now();
return {
@ -38,6 +43,7 @@ function getTransformConfig(): TransformPivotConfig {
retention_policy: { time: { field: 'order_date', max_age: '1d' } },
settings: {
max_page_search_size: 250,
num_failure_retries: 0,
},
dest: { index: `user-ec_2_${date}` },
};
@ -76,6 +82,7 @@ function getTransformConfigWithRuntimeMappings(): TransformPivotConfig {
retention_policy: { time: { field: 'order_date', max_age: '3d' } },
settings: {
max_page_search_size: 250,
num_failure_retries: 5,
},
dest: { index: `user-ec_2_${date}` },
};
@ -161,6 +168,9 @@ export default function ({ getService }: FtrProviderContext) {
retentionPolicySwitchEnabled: true,
retentionPolicyField: 'order_date',
retentionPolicyMaxAge: '1d',
numFailureRetries: getNumFailureRetriesStr(
transformConfigWithPivot.settings?.num_failure_retries
),
},
},
{
@ -193,6 +203,9 @@ export default function ({ getService }: FtrProviderContext) {
retentionPolicySwitchEnabled: true,
retentionPolicyField: 'order_date',
retentionPolicyMaxAge: '3d',
numFailureRetries: getNumFailureRetriesStr(
transformConfigWithRuntimeMapping.settings?.num_failure_retries
),
},
},
{
@ -366,10 +379,23 @@ export default function ({ getService }: FtrProviderContext) {
await transform.wizard.assertTransformMaxPageSearchSizeValue(
testData.originalConfig.settings!.max_page_search_size!
);
if (testData.expected.numFailureRetries !== undefined) {
await transform.wizard.assertNumFailureRetriesValue(
testData.expected.numFailureRetries
);
}
await transform.testExecution.logTestStep('should load the create step');
await transform.wizard.advanceToCreateStep();
if (testData.expected.numFailureRetries !== undefined) {
await transform.testExecution.logTestStep('displays the summary details');
await transform.wizard.openTransformAdvancedSettingsSummaryAccordion();
await transform.wizard.assertTransformNumFailureRetriesSummaryValue(
testData.expected.numFailureRetries
);
}
await transform.testExecution.logTestStep('should display the create and start button');
await transform.wizard.assertCreateAndStartButtonExists();
await transform.wizard.assertCreateAndStartButtonEnabled(true);

View file

@ -35,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) {
await transform.testResources.deleteIndexPatternByTitle('ft_ecommerce');
});
const DEFAULT_NUM_FAILURE_RETRIES = '5';
const testDataList: Array<PivotTransformTestData | LatestTransformTestData> = [
{
type: 'pivot',
@ -92,6 +93,7 @@ export default function ({ getService }: FtrProviderContext) {
return `user-${this.transformId}`;
},
discoverAdjustSuperDatePicker: true,
numFailureRetries: '7',
expected: {
pivotAdvancedEditorValueArr: ['{', ' "group_by": {', ' "category": {'],
pivotAdvancedEditorValue: {
@ -250,6 +252,7 @@ export default function ({ getService }: FtrProviderContext) {
},
],
discoverQueryHits: '7,270',
numFailureRetries: '7',
},
} as PivotTransformTestData,
{
@ -288,6 +291,7 @@ export default function ({ getService }: FtrProviderContext) {
return `user-${this.transformId}`;
},
discoverAdjustSuperDatePicker: false,
numFailureRetries: '-1',
expected: {
pivotAdvancedEditorValueArr: ['{', ' "group_by": {', ' "geoip.country_iso_code": {'],
pivotAdvancedEditorValue: {
@ -335,6 +339,7 @@ export default function ({ getService }: FtrProviderContext) {
rows: 5,
},
discoverQueryHits: '10',
numFailureRetries: '-1',
},
} as PivotTransformTestData,
{
@ -360,6 +365,7 @@ export default function ({ getService }: FtrProviderContext) {
return `user-${this.transformId}`;
},
discoverAdjustSuperDatePicker: false,
numFailureRetries: '0',
expected: {
pivotAdvancedEditorValueArr: ['{', ' "group_by": {', ' "customer_gender": {'],
pivotAdvancedEditorValue: {
@ -393,6 +399,7 @@ export default function ({ getService }: FtrProviderContext) {
rows: 5,
},
discoverQueryHits: '2',
numFailureRetries: '0',
},
} as PivotTransformTestData,
{
@ -418,6 +425,7 @@ export default function ({ getService }: FtrProviderContext) {
},
destinationDataViewTimeField: 'order_date',
discoverAdjustSuperDatePicker: true,
numFailureRetries: '101',
expected: {
latestPreview: {
column: 0,
@ -443,6 +451,7 @@ export default function ({ getService }: FtrProviderContext) {
],
},
discoverQueryHits: '10',
numFailureRetries: 'error',
},
} as LatestTransformTestData,
];
@ -602,9 +611,40 @@ export default function ({ getService }: FtrProviderContext) {
await transform.wizard.assertContinuousModeSwitchExists();
await transform.wizard.assertContinuousModeSwitchCheckState(false);
await transform.testExecution.logTestStep(
'should display the advanced settings and show pre-filled configuration'
);
await transform.wizard.openTransformAdvancedSettingsAccordion();
if (
testData.numFailureRetries !== undefined &&
testData.expected.numFailureRetries !== undefined
) {
await transform.wizard.assertNumFailureRetriesValue('');
await transform.wizard.setTransformNumFailureRetriesValue(
testData.numFailureRetries.toString(),
testData.expected.numFailureRetries
);
// If num failure input is expected to give an error, sets it back to a valid
// so that we can continue creating the transform
if (testData.expected.numFailureRetries === 'error') {
await transform.wizard.setTransformNumFailureRetriesValue(
DEFAULT_NUM_FAILURE_RETRIES,
DEFAULT_NUM_FAILURE_RETRIES
);
}
}
await transform.testExecution.logTestStep('loads the create step');
await transform.wizard.advanceToCreateStep();
await transform.testExecution.logTestStep('displays the summary details');
await transform.wizard.openTransformAdvancedSettingsSummaryAccordion();
await transform.wizard.assertTransformNumFailureRetriesSummaryValue(
testData.expected.numFailureRetries === 'error'
? DEFAULT_NUM_FAILURE_RETRIES
: testData.expected.numFailureRetries
);
await transform.testExecution.logTestStep('displays the create and start button');
await transform.wizard.assertCreateAndStartButtonExists();
await transform.wizard.assertCreateAndStartButtonEnabled(true);

View file

@ -61,6 +61,7 @@ export default function ({ getService }: FtrProviderContext) {
resetRetentionPolicy: false,
transformRetentionPolicyField: 'order_date',
transformRetentionPolicyMaxAge: '1d',
numFailureRetries: '0',
expected: {
messageText: 'updated transform.',
retentionPolicy: {
@ -82,6 +83,7 @@ export default function ({ getService }: FtrProviderContext) {
transformDocsPerSecond: '1000',
transformFrequency: '10m',
resetRetentionPolicy: true,
numFailureRetries: '7',
expected: {
messageText: 'updated transform.',
retentionPolicy: {
@ -145,6 +147,17 @@ export default function ({ getService }: FtrProviderContext) {
testData.transformDocsPerSecond
);
await transform.testExecution.logTestStep(
'should update the transform number of failure retries'
);
await transform.editFlyout.openTransformEditAccordionAdvancedSettings();
await transform.editFlyout.assertTransformEditFlyoutInputExists('NumFailureRetries');
await transform.editFlyout.assertTransformEditFlyoutInputValue('NumFailureRetries', '');
await transform.editFlyout.setTransformEditFlyoutInputValue(
'NumFailureRetries',
testData.numFailureRetries
);
await transform.testExecution.logTestStep('should update the transform frequency');
await transform.editFlyout.assertTransformEditFlyoutInputExists('Frequency');
await transform.editFlyout.assertTransformEditFlyoutInputValue(

View file

@ -67,6 +67,7 @@ export interface BaseTransformTestData {
destinationIndex: string;
destinationDataViewTimeField?: string;
discoverAdjustSuperDatePicker: boolean;
numFailureRetries?: string;
}
export interface PivotTransformTestData extends BaseTransformTestData {

View file

@ -24,6 +24,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi
const testSubjects = getService('testSubjects');
const comboBox = getService('comboBox');
const retry = getService('retry');
const find = getService('find');
const ml = getService('ml');
const PageObjects = getPageObjects(['discover', 'timePicker', 'unifiedSearch']);
@ -798,6 +799,72 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi
);
},
async assertTransformNumFailureRetriesInputExists() {
await testSubjects.existOrFail('transformNumFailureRetriesInput');
expect(await testSubjects.isDisplayed('transformNumFailureRetriesInput')).to.eql(
true,
`Expected 'Number of retries failure' input to be displayed`
);
},
async assertNumFailureRetriesValue(expectedValue: string) {
await this.assertTransformNumFailureRetriesInputExists();
const actualValue = await testSubjects.getAttribute(
'transformNumFailureRetriesInput',
'value'
);
expect(actualValue).to.eql(
expectedValue,
`Transform num failure retries text should be '${expectedValue}' (got '${actualValue}')`
);
},
async assertTransformNumFailureRetriesErrorMessageExists(expected: boolean) {
const row = await testSubjects.find('transformNumFailureRetriesFormRow');
const errorElements = await row.findAllByClassName('euiFormErrorText');
if (expected) {
expect(errorElements.length).greaterThan(
0,
'Expected Transform num failure retries to display error message'
);
} else {
expect(errorElements.length).eql(
0,
'Expected Transform num failure retries to not display error message'
);
}
},
async setTransformNumFailureRetriesValue(value: string, expectedResult: 'error' | string) {
await retry.tryForTime(5000, async () => {
await testSubjects.setValue('transformNumFailureRetriesInput', value);
if (expectedResult !== 'error') {
await this.assertTransformNumFailureRetriesErrorMessageExists(false);
await this.assertNumFailureRetriesValue(expectedResult);
} else {
await this.assertTransformNumFailureRetriesErrorMessageExists(true);
}
});
},
async assertTransformNumFailureRetriesSummaryValue(expectedValue: string) {
await retry.tryForTime(5000, async () => {
await testSubjects.existOrFail('transformWizardAdvancedSettingsNumFailureRetriesLabel');
const row = await testSubjects.find(
'transformWizardAdvancedSettingsNumFailureRetriesLabel'
);
const actualValue = await (
await row.findByClassName('euiFormRow__fieldWrapper')
).getVisibleText();
expect(actualValue).to.eql(
expectedValue,
`Transform num failure retries summary value should be '${expectedValue}' (got '${actualValue}')`
);
});
},
async assertTransformMaxPageSearchSizeInputExists() {
await testSubjects.existOrFail('transformMaxPageSearchSizeInput');
expect(await testSubjects.isDisplayed('transformMaxPageSearchSizeInput')).to.eql(
@ -817,6 +884,22 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi
);
},
async assertAccordionAdvancedSettingsSummaryAccordionExists() {
await testSubjects.existOrFail('transformWizardAccordionAdvancedSettingsSummary');
},
// for now we expect this to be used only for opening the accordion
async openTransformAdvancedSettingsSummaryAccordion() {
await this.assertAccordionAdvancedSettingsSummaryAccordionExists();
await find.clickByCssSelector(
'[aria-controls="transformWizardAccordionAdvancedSettingsSummary"]'
);
await testSubjects.existOrFail('transformWizardAdvancedSettingsFrequencyLabel');
await testSubjects.existOrFail('transformWizardAdvancedSettingsMaxPageSearchSizeLabel');
await testSubjects.existOrFail('transformWizardAdvancedSettingsNumFailureRetriesLabel');
},
async assertCreateAndStartButtonExists() {
await testSubjects.existOrFail('transformWizardCreateAndStartButton');
expect(await testSubjects.isDisplayed('transformWizardCreateAndStartButton')).to.eql(