[ILM] Add UI validation for min age value (#96718)

This commit is contained in:
Sébastien Loix 2021-04-13 18:47:20 +01:00 committed by GitHub
parent 8e9ca66520
commit 67e512fe27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 341 additions and 47 deletions

View file

@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
import { ComponentType, ReactWrapper } from 'enzyme';
import { Component as ReactComponent } from 'react';
import { ComponentType, HTMLAttributes, ReactWrapper } from 'enzyme';
import { findTestSubject } from '../find_test_subject';
import { reactRouterMock } from '../router_helpers';
@ -250,8 +251,17 @@ export const registerTestBed = <T extends string = string>(
component.update();
};
const getErrorsMessages: TestBed<T>['form']['getErrorsMessages'] = () => {
const errorMessagesWrappers = component.find('.euiFormErrorText');
const getErrorsMessages: TestBed<T>['form']['getErrorsMessages'] = (
wrapper?: T | ReactWrapper
) => {
let errorMessagesWrappers: ReactWrapper<HTMLAttributes, any, ReactComponent>;
if (typeof wrapper === 'string') {
errorMessagesWrappers = find(wrapper).find('.euiFormErrorText');
} else {
errorMessagesWrappers = wrapper
? wrapper.find('.euiFormErrorText')
: component.find('.euiFormErrorText');
}
return errorMessagesWrappers.map((err) => err.text());
};

View file

@ -133,7 +133,7 @@ export interface TestBed<T = string> {
/**
* Get a list of the form error messages that are visible in the DOM.
*/
getErrorsMessages: () => string[];
getErrorsMessages: (wrapper?: T | ReactWrapper) => string[];
};
table: {
getMetaData: (tableTestSubject: T) => EuiTableMetaData;

View file

@ -29,6 +29,7 @@ export const POLICY_WITH_MIGRATE_OFF: PolicyFromES = {
},
},
warm: {
min_age: '1d',
actions: {
migrate: { enabled: false },
},
@ -54,6 +55,7 @@ export const POLICY_WITH_INCLUDE_EXCLUDE: PolicyFromES = {
},
},
warm: {
min_age: '10d',
actions: {
allocate: {
include: {
@ -196,6 +198,7 @@ export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = ({
},
},
warm: {
min_age: '10d',
actions: {
my_unfollow_action: {},
set_priority: {
@ -205,6 +208,7 @@ export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = ({
},
},
delete: {
min_age: '15d',
wait_for_snapshot: {
policy: SNAPSHOT_POLICY_NAME,
},

View file

@ -320,10 +320,8 @@ export const setup = async (arg?: {
};
/*
* For new we rely on a setTimeout to ensure that error messages have time to populate
* the form object before we look at the form object. See:
* x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx
* for where this logic lives.
* We rely on a setTimeout (dedounce) to display error messages under the form fields.
* This handler runs all the timers so we can assert for errors in our tests.
*/
const runTimers = () => {
act(() => {

View file

@ -77,8 +77,10 @@ describe('<EditPolicy /> searchable snapshots', () => {
const repository = 'myRepo';
await actions.hot.setSearchableSnapshot(repository);
await actions.cold.enable(true);
await actions.cold.setMinAgeValue('10');
await actions.cold.toggleSearchableSnapshot(true);
await actions.frozen.enable(true);
await actions.frozen.setMinAgeValue('15');
await actions.savePolicy();
const latestRequest = server.requests[server.requests.length - 1];
@ -96,8 +98,10 @@ describe('<EditPolicy /> searchable snapshots', () => {
await actions.hot.setSearchableSnapshot('myRepo');
await actions.cold.enable(true);
await actions.cold.setMinAgeValue('10');
await actions.cold.toggleSearchableSnapshot(true);
await actions.frozen.enable(true);
await actions.frozen.setMinAgeValue('15');
// We update the repository in one phase
await actions.frozen.setSearchableSnapshot('changed');
@ -161,6 +165,7 @@ describe('<EditPolicy /> searchable snapshots', () => {
test('correctly sets snapshot repository default to "found-snapshots"', async () => {
const { actions } = testBed;
await actions.cold.enable(true);
await actions.cold.setMinAgeValue('10');
await actions.cold.toggleSearchableSnapshot(true);
await actions.savePolicy();
const latestRequest = server.requests[server.requests.length - 1];

View file

@ -56,7 +56,6 @@ describe('<EditPolicy /> error indicators', () => {
const { actions } = testBed;
// 0. No validation issues
expect(actions.hasGlobalErrorCallout()).toBe(false);
expect(actions.hot.hasErrorIndicator()).toBe(false);
expect(actions.warm.hasErrorIndicator()).toBe(false);
expect(actions.cold.hasErrorIndicator()).toBe(false);
@ -65,7 +64,6 @@ describe('<EditPolicy /> error indicators', () => {
await actions.hot.toggleForceMerge(true);
await actions.hot.setForcemergeSegmentsCount('-22');
runTimers();
expect(actions.hasGlobalErrorCallout()).toBe(true);
expect(actions.hot.hasErrorIndicator()).toBe(true);
expect(actions.warm.hasErrorIndicator()).toBe(false);
expect(actions.cold.hasErrorIndicator()).toBe(false);
@ -75,7 +73,6 @@ describe('<EditPolicy /> error indicators', () => {
await actions.warm.toggleForceMerge(true);
await actions.warm.setForcemergeSegmentsCount('-22');
runTimers();
expect(actions.hasGlobalErrorCallout()).toBe(true);
expect(actions.hot.hasErrorIndicator()).toBe(true);
expect(actions.warm.hasErrorIndicator()).toBe(true);
expect(actions.cold.hasErrorIndicator()).toBe(false);
@ -84,7 +81,6 @@ describe('<EditPolicy /> error indicators', () => {
await actions.cold.enable(true);
await actions.cold.setReplicas('-33');
runTimers();
expect(actions.hasGlobalErrorCallout()).toBe(true);
expect(actions.hot.hasErrorIndicator()).toBe(true);
expect(actions.warm.hasErrorIndicator()).toBe(true);
expect(actions.cold.hasErrorIndicator()).toBe(true);
@ -92,7 +88,6 @@ describe('<EditPolicy /> error indicators', () => {
// 4. Fix validation issue in hot
await actions.hot.setForcemergeSegmentsCount('1');
runTimers();
expect(actions.hasGlobalErrorCallout()).toBe(true);
expect(actions.hot.hasErrorIndicator()).toBe(false);
expect(actions.warm.hasErrorIndicator()).toBe(true);
expect(actions.cold.hasErrorIndicator()).toBe(true);
@ -100,7 +95,6 @@ describe('<EditPolicy /> error indicators', () => {
// 5. Fix validation issue in warm
await actions.warm.setForcemergeSegmentsCount('1');
runTimers();
expect(actions.hasGlobalErrorCallout()).toBe(true);
expect(actions.hot.hasErrorIndicator()).toBe(false);
expect(actions.warm.hasErrorIndicator()).toBe(false);
expect(actions.cold.hasErrorIndicator()).toBe(true);
@ -108,13 +102,12 @@ describe('<EditPolicy /> error indicators', () => {
// 6. Fix validation issue in cold
await actions.cold.setReplicas('1');
runTimers();
expect(actions.hasGlobalErrorCallout()).toBe(false);
expect(actions.hot.hasErrorIndicator()).toBe(false);
expect(actions.warm.hasErrorIndicator()).toBe(false);
expect(actions.cold.hasErrorIndicator()).toBe(false);
});
test('global error callout should show if there are any form errors', async () => {
test('global error callout should show, after clicking the "Save" button, if there are any form errors', async () => {
const { actions } = testBed;
expect(actions.hasGlobalErrorCallout()).toBe(false);
@ -125,6 +118,7 @@ describe('<EditPolicy /> error indicators', () => {
await actions.saveAsNewPolicy(true);
await actions.setPolicyName('');
runTimers();
await actions.savePolicy();
expect(actions.hasGlobalErrorCallout()).toBe(true);
expect(actions.hot.hasErrorIndicator()).toBe(false);
@ -136,6 +130,7 @@ describe('<EditPolicy /> error indicators', () => {
const { actions } = testBed;
await actions.cold.enable(true);
await actions.cold.setMinAgeValue('7');
// introduce validation error
await actions.cold.setSearchableSnapshot('');
runTimers();

View file

@ -81,6 +81,10 @@ describe('<EditPolicy /> timing validation', () => {
test(`${phase}: ${name}`, async () => {
const { actions } = testBed;
await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].enable(true);
// 1. We first set as dummy value to have a starting min_age value
await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].setMinAgeValue('111');
// 2. At this point we are sure there will be a change of value and that any validation
// will be displayed under the field.
await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].setMinAgeValue(value);
runTimers();
@ -89,4 +93,52 @@ describe('<EditPolicy /> timing validation', () => {
});
});
});
test('should validate that min_age is equal or greater than previous phase min_age', async () => {
const { actions, form } = testBed;
await actions.warm.enable(true);
await actions.cold.enable(true);
await actions.frozen.enable(true);
await actions.delete.enable(true);
await actions.warm.setMinAgeValue('10');
await actions.cold.setMinAgeValue('9');
runTimers();
expect(form.getErrorsMessages('cold-phase')).toEqual([
'Must be greater or equal than the warm phase value (10d)',
]);
await actions.frozen.setMinAgeValue('8');
runTimers();
expect(form.getErrorsMessages('frozen-phase')).toEqual([
'Must be greater or equal than the cold phase value (9d)',
]);
await actions.delete.setMinAgeValue('7');
runTimers();
expect(form.getErrorsMessages('delete-phaseContent')).toEqual([
'Must be greater or equal than the frozen phase value (8d)',
]);
// Disable the warm phase
await actions.warm.enable(false);
// No more error for the cold phase
expect(form.getErrorsMessages('cold-phase')).toEqual([]);
// Change to smaller unit for cold phase
await actions.cold.setMinAgeUnits('h');
// No more error for the frozen phase...
expect(form.getErrorsMessages('frozen-phase')).toEqual([]);
// ...but the delete phase has still the error
expect(form.getErrorsMessages('delete-phaseContent')).toEqual([
'Must be greater or equal than the frozen phase value (8d)',
]);
await actions.delete.setMinAgeValue('9');
// No more error for the delete phase
expect(form.getErrorsMessages('delete-phaseContent')).toEqual([]);
});
});

View file

@ -87,7 +87,7 @@ describe('<EditPolicy /> serialization', () => {
unknown_setting: true,
},
},
min_age: '0d',
min_age: '10d',
},
},
});
@ -264,6 +264,7 @@ describe('<EditPolicy /> serialization', () => {
test('default values', async () => {
const { actions } = testBed;
await actions.warm.enable(true);
await actions.warm.setMinAgeValue('11');
await actions.savePolicy();
const latestRequest = server.requests[server.requests.length - 1];
const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm;
@ -274,7 +275,7 @@ describe('<EditPolicy /> serialization', () => {
"priority": 50,
},
},
"min_age": "0d",
"min_age": "11d",
}
`);
});
@ -282,6 +283,7 @@ describe('<EditPolicy /> serialization', () => {
test('setting all values', async () => {
const { actions } = testBed;
await actions.warm.enable(true);
await actions.warm.setMinAgeValue('11');
await actions.warm.setDataAllocation('node_attrs');
await actions.warm.setSelectedNodeAttribute('test:123');
await actions.warm.setReplicas('123');
@ -329,7 +331,7 @@ describe('<EditPolicy /> serialization', () => {
"number_of_shards": 123,
},
},
"min_age": "0d",
"min_age": "11d",
},
},
}
@ -401,6 +403,7 @@ describe('<EditPolicy /> serialization', () => {
const { actions } = testBed;
await actions.cold.enable(true);
await actions.cold.setMinAgeValue('11');
await actions.savePolicy();
const latestRequest = server.requests[server.requests.length - 1];
const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
@ -411,7 +414,7 @@ describe('<EditPolicy /> serialization', () => {
"priority": 0,
},
},
"min_age": "0d",
"min_age": "11d",
}
`);
});
@ -471,6 +474,7 @@ describe('<EditPolicy /> serialization', () => {
test('setting searchable snapshot', async () => {
const { actions } = testBed;
await actions.cold.enable(true);
await actions.cold.setMinAgeValue('10');
await actions.cold.setSearchableSnapshot('my-repo');
await actions.savePolicy();
const latestRequest2 = server.requests[server.requests.length - 1];
@ -485,6 +489,7 @@ describe('<EditPolicy /> serialization', () => {
test('default value', async () => {
const { actions } = testBed;
await actions.frozen.enable(true);
await actions.frozen.setMinAgeValue('13');
await actions.frozen.setSearchableSnapshot('myRepo');
await actions.savePolicy();
@ -492,7 +497,7 @@ describe('<EditPolicy /> serialization', () => {
const latestRequest = server.requests[server.requests.length - 1];
const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
expect(entirePolicy.phases.frozen).toEqual({
min_age: '0d',
min_age: '13d',
actions: {
searchable_snapshot: { snapshot_repository: 'myRepo' },
},

View file

@ -25,9 +25,10 @@ const i18nTexts = {
export const FormErrorsCallout: FunctionComponent = () => {
const {
errors: { hasErrors },
isFormSubmitted,
} = useFormErrorsContext();
if (!hasErrors) {
if (!isFormSubmitted || !hasErrors) {
return null;
}

View file

@ -6,8 +6,9 @@
*/
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { get } from 'lodash';
import {
EuiFieldNumber,
@ -20,10 +21,9 @@ import {
EuiIconTip,
} from '@elastic/eui';
import { getFieldValidityAndErrorMessage } from '../../../../../../../shared_imports';
import { UseField, useConfiguration } from '../../../../form';
import { getFieldValidityAndErrorMessage, useFormData } from '../../../../../../../shared_imports';
import { UseField, useConfiguration, useGlobalFields } from '../../../../form';
import { getPhaseMinAgeInMilliseconds } from '../../../../lib';
import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util';
type PhaseWithMinAgeAction = 'warm' | 'cold' | 'delete';
@ -81,9 +81,43 @@ interface Props {
}
export const MinAgeField: FunctionComponent<Props> = ({ phase }): React.ReactElement => {
const minAgeValuePath = `phases.${phase}.min_age`;
const minAgeUnitPath = `_meta.${phase}.minAgeUnit`;
const { isUsingRollover } = useConfiguration();
const globalFields = useGlobalFields();
const { setValue: setMillisecondValue } = globalFields[
`${phase}MinAgeMilliSeconds` as 'coldMinAgeMilliSeconds'
];
const [formData] = useFormData({ watch: [minAgeValuePath, minAgeUnitPath] });
const minAgeValue = get(formData, minAgeValuePath);
const minAgeUnit = get(formData, minAgeUnitPath);
useEffect(() => {
// Whenever the min_age value of the field OR the min_age unit
// changes, we update the corresponding millisecond global field for the phase
if (minAgeValue === undefined) {
return;
}
const milliseconds =
minAgeValue.trim() === '' ? -1 : getPhaseMinAgeInMilliseconds(minAgeValue, minAgeUnit);
setMillisecondValue(milliseconds);
}, [minAgeValue, minAgeUnit, setMillisecondValue]);
useEffect(() => {
return () => {
// When unmounting (meaning we have disabled the phase), we remove
// the millisecond value so the next time we enable the phase it will
// be updated and trigger the validation
setMillisecondValue(-1);
};
}, [setMillisecondValue]);
return (
<UseField path={`phases.${phase}.min_age`}>
<UseField path={minAgeValuePath}>
{(field) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
return (
@ -118,7 +152,7 @@ export const MinAgeField: FunctionComponent<Props> = ({ phase }): React.ReactEle
/>
</EuiFlexItem>
<EuiFlexItem grow={true} style={{ minWidth: 165 }}>
<UseField path={`_meta.${phase}.minAgeUnit`}>
<UseField path={minAgeUnitPath}>
{(unitField) => {
const { isInvalid: isUnitFieldInvalid } = getFieldValidityAndErrorMessage(
unitField

View file

@ -46,20 +46,24 @@ export const createDeserializer = (isCloudEnabled: boolean) => (
bestCompression: warm?.actions?.forcemerge?.index_codec === 'best_compression',
dataTierAllocationType: determineDataTierAllocationType(warm?.actions),
readonlyEnabled: Boolean(warm?.actions?.readonly),
minAgeToMilliSeconds: -1,
},
cold: {
enabled: Boolean(cold),
dataTierAllocationType: determineDataTierAllocationType(cold?.actions),
freezeEnabled: Boolean(cold?.actions?.freeze),
readonlyEnabled: Boolean(cold?.actions?.readonly),
minAgeToMilliSeconds: -1,
},
frozen: {
enabled: Boolean(frozen),
dataTierAllocationType: determineDataTierAllocationType(frozen?.actions),
freezeEnabled: Boolean(frozen?.actions?.freeze),
minAgeToMilliSeconds: -1,
},
delete: {
enabled: Boolean(deletePhase),
minAgeToMilliSeconds: -1,
},
searchableSnapshot: {
repository: defaultRepository,

View file

@ -38,6 +38,7 @@ interface ContextValue {
errors: Errors;
addError(phase: PhasesAndOther, fieldPath: string, errorMessages: string[]): void;
clearError(phase: PhasesAndOther, fieldPath: string): void;
isFormSubmitted: boolean;
}
const FormErrorsContext = createContext<ContextValue>(null as any);
@ -56,7 +57,7 @@ export const FormErrorsProvider: FunctionComponent = ({ children }) => {
const [errors, setErrors] = useState<Errors>(createEmptyErrors);
const form = useFormContext<FormInternal>();
const { getErrors: getFormErrors } = form;
const { getErrors: getFormErrors, isSubmitted } = form;
const addError: ContextValue['addError'] = useCallback(
(phase, fieldPath, errorMessages) => {
@ -83,9 +84,9 @@ export const FormErrorsProvider: FunctionComponent = ({ children }) => {
} = previousErrors;
const nextHasErrors =
Object.keys(restOfPhaseErrors).length === 0 &&
Object.keys(restOfPhaseErrors).length > 0 ||
Object.values(otherPhases).some((phaseErrors) => {
return !!Object.keys(phaseErrors).length;
return Object.keys(phaseErrors).length > 0;
});
return {
@ -107,6 +108,7 @@ export const FormErrorsProvider: FunctionComponent = ({ children }) => {
errors,
addError,
clearError,
isFormSubmitted: isSubmitted,
}}
>
{children}

View file

@ -14,6 +14,10 @@ import { UseMultiFields, FieldHook, FieldConfig } from '../../../../shared_impor
interface GlobalFieldsTypes {
deleteEnabled: boolean;
searchableSnapshotRepo: string;
warmMinAgeMilliSeconds: number;
coldMinAgeMilliSeconds: number;
frozenMinAgeMilliSeconds: number;
deleteMinAgeMilliSeconds: number;
}
type GlobalFields = {
@ -32,6 +36,18 @@ export const globalFields: Record<
searchableSnapshotRepo: {
path: '_meta.searchableSnapshot.repository',
},
warmMinAgeMilliSeconds: {
path: '_meta.warm.minAgeToMilliSeconds',
},
coldMinAgeMilliSeconds: {
path: '_meta.cold.minAgeToMilliSeconds',
},
frozenMinAgeMilliSeconds: {
path: '_meta.frozen.minAgeToMilliSeconds',
},
deleteMinAgeMilliSeconds: {
path: '_meta.delete.minAgeToMilliSeconds',
},
};
export const GlobalFieldsProvider: FunctionComponent = ({ children }) => {

View file

@ -10,12 +10,14 @@ import { i18n } from '@kbn/i18n';
import { FormSchema, fieldValidators } from '../../../../shared_imports';
import { defaultIndexPriority } from '../../../constants';
import { ROLLOVER_FORM_PATHS, CLOUD_DEFAULT_REPO } from '../constants';
import { MinAgePhase } from '../types';
import { i18nTexts } from '../i18n_texts';
import {
ifExistsNumberGreaterThanZero,
ifExistsNumberNonNegative,
rolloverThresholdsValidator,
integerValidator,
minAgeGreaterThanPreviousPhase,
} from './validations';
const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS);
@ -117,8 +119,11 @@ const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({
serializer: serializers.stringToNumber,
});
const getMinAgeField = (defaultValue: string = '0') => ({
const getMinAgeField = (phase: MinAgePhase, defaultValue?: string) => ({
defaultValue,
// By passing an empty array we make sure to *not* trigger the validation when the field value changes.
// The validation will be triggered when the millisecond variant (in the _meta) is updated (in sync)
fieldsToValidateOnChange: [],
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
@ -129,8 +134,12 @@ const getMinAgeField = (defaultValue: string = '0') => ({
{
validator: integerValidator,
},
{
validator: minAgeGreaterThanPreviousPhase(phase),
},
],
});
export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
_meta: {
hot: {
@ -173,6 +182,15 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
minAgeUnit: {
defaultValue: 'd',
},
minAgeToMilliSeconds: {
defaultValue: -1,
fieldsToValidateOnChange: [
'phases.warm.min_age',
'phases.cold.min_age',
'phases.frozen.min_age',
'phases.delete.min_age',
],
},
bestCompression: {
label: i18nTexts.editPolicy.bestCompressionFieldLabel,
},
@ -208,6 +226,14 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
minAgeUnit: {
defaultValue: 'd',
},
minAgeToMilliSeconds: {
defaultValue: -1,
fieldsToValidateOnChange: [
'phases.cold.min_age',
'phases.frozen.min_age',
'phases.delete.min_age',
],
},
dataTierAllocationType: {
label: i18nTexts.editPolicy.allocationTypeOptionsFieldLabel,
},
@ -232,6 +258,10 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
minAgeUnit: {
defaultValue: 'd',
},
minAgeToMilliSeconds: {
defaultValue: -1,
fieldsToValidateOnChange: ['phases.frozen.min_age', 'phases.delete.min_age'],
},
dataTierAllocationType: {
label: i18nTexts.editPolicy.allocationTypeOptionsFieldLabel,
},
@ -250,6 +280,10 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
minAgeUnit: {
defaultValue: 'd',
},
minAgeToMilliSeconds: {
defaultValue: -1,
fieldsToValidateOnChange: ['phases.delete.min_age'],
},
},
searchableSnapshot: {
repository: {
@ -324,7 +358,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
},
},
warm: {
min_age: getMinAgeField(),
min_age: getMinAgeField('warm'),
actions: {
allocate: {
number_of_replicas: numberOfReplicasField,
@ -341,7 +375,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
},
},
cold: {
min_age: getMinAgeField(),
min_age: getMinAgeField('cold'),
actions: {
allocate: {
number_of_replicas: numberOfReplicasField,
@ -353,7 +387,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
},
},
frozen: {
min_age: getMinAgeField(),
min_age: getMinAgeField('frozen'),
actions: {
allocate: {
number_of_replicas: numberOfReplicasField,
@ -365,7 +399,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
},
},
delete: {
min_age: getMinAgeField('365'),
min_age: getMinAgeField('delete', '365'),
actions: {
wait_for_snapshot: {
policy: {

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { fieldValidators, ValidationFunc, ValidationConfig } from '../../../../shared_imports';
@ -11,7 +12,7 @@ import { ROLLOVER_FORM_PATHS } from '../constants';
import { i18nTexts } from '../i18n_texts';
import { PolicyFromES } from '../../../../../common/types';
import { FormInternal } from '../types';
import { FormInternal, MinAgePhase } from '../types';
const { numberGreaterThanField, containsCharsField, emptyField, startsWithField } = fieldValidators;
@ -149,3 +150,117 @@ export const createPolicyNameValidations = ({
},
];
};
/**
* This validator guarantees that the user does not specify a min_age
* value smaller that the min_age of a previous phase.
* For example, the user can't define '5 days' for cold phase if the
* warm phase is set to '10 days'.
*/
export const minAgeGreaterThanPreviousPhase = (phase: MinAgePhase) => ({
formData,
}: {
formData: Record<string, number>;
}) => {
if (phase === 'warm') {
return;
}
const getValueFor = (_phase: MinAgePhase) => {
const milli = formData[`_meta.${_phase}.minAgeToMilliSeconds`];
const esFormat =
milli >= 0
? formData[`phases.${_phase}.min_age`] + formData[`_meta.${_phase}.minAgeUnit`]
: undefined;
return {
milli,
esFormat,
};
};
const minAgeValues = {
warm: getValueFor('warm'),
cold: getValueFor('cold'),
frozen: getValueFor('frozen'),
delete: getValueFor('delete'),
};
const i18nErrors = {
greaterThanWarmPhase: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.minAgeSmallerThanWarmPhaseError',
{
defaultMessage: 'Must be greater or equal than the warm phase value ({value})',
values: {
value: minAgeValues.warm.esFormat,
},
}
),
greaterThanColdPhase: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.minAgeSmallerThanColdPhaseError',
{
defaultMessage: 'Must be greater or equal than the cold phase value ({value})',
values: {
value: minAgeValues.cold.esFormat,
},
}
),
greaterThanFrozenPhase: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.minAgeSmallerThanFrozenPhaseError',
{
defaultMessage: 'Must be greater or equal than the frozen phase value ({value})',
values: {
value: minAgeValues.frozen.esFormat,
},
}
),
};
if (phase === 'cold') {
if (minAgeValues.warm.milli >= 0 && minAgeValues.cold.milli < minAgeValues.warm.milli) {
return {
message: i18nErrors.greaterThanWarmPhase,
};
}
return;
}
if (phase === 'frozen') {
if (minAgeValues.cold.milli >= 0 && minAgeValues.frozen.milli < minAgeValues.cold.milli) {
return {
message: i18nErrors.greaterThanColdPhase,
};
} else if (
minAgeValues.warm.milli >= 0 &&
minAgeValues.frozen.milli < minAgeValues.warm.milli
) {
return {
message: i18nErrors.greaterThanWarmPhase,
};
}
return;
}
if (phase === 'delete') {
if (minAgeValues.frozen.milli >= 0 && minAgeValues.delete.milli < minAgeValues.frozen.milli) {
return {
message: i18nErrors.greaterThanFrozenPhase,
};
} else if (
minAgeValues.cold.milli >= 0 &&
minAgeValues.delete.milli < minAgeValues.cold.milli
) {
return {
message: i18nErrors.greaterThanColdPhase,
};
} else if (
minAgeValues.warm.milli >= 0 &&
minAgeValues.delete.milli < minAgeValues.warm.milli
) {
return {
message: i18nErrors.greaterThanWarmPhase,
};
}
}
};

View file

@ -24,12 +24,10 @@ import moment from 'moment';
import { splitSizeAndUnits } from '../../../lib/policies';
import { FormInternal } from '../types';
import { FormInternal, MinAgePhase } from '../types';
/* -===- Private functions and types -===- */
type MinAgePhase = 'warm' | 'cold' | 'frozen' | 'delete';
type Phase = 'hot' | MinAgePhase;
const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'frozen', 'delete'];
@ -44,9 +42,9 @@ const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({
* See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math
* for all date math values. ILM policies also support "micros" and "nanos".
*/
const getPhaseMinAgeInMilliseconds = (phase: { min_age: string }): number => {
export const getPhaseMinAgeInMilliseconds = (size: string, units: string): number => {
let milliseconds: number;
const { units, size } = splitSizeAndUnits(phase.min_age);
if (units === 'micros') {
milliseconds = parseInt(size, 10) / 1e3;
} else if (units === 'nanos') {
@ -126,7 +124,10 @@ export const calculateRelativeFromAbsoluteMilliseconds = (
// If we have a next phase, calculate the timing between this phase and the next
if (nextPhase && inputs[nextPhase]?.min_age) {
nextPhaseMinAge = getPhaseMinAgeInMilliseconds(inputs[nextPhase] as { min_age: string });
const { units, size } = splitSizeAndUnits(
(inputs[nextPhase] as { min_age: string }).min_age
);
nextPhaseMinAge = getPhaseMinAgeInMilliseconds(size, units);
}
return {

View file

@ -8,6 +8,7 @@
export {
calculateRelativeFromAbsoluteMilliseconds,
formDataToAbsoluteTimings,
getPhaseMinAgeInMilliseconds,
AbsoluteTimings,
PhaseAgeInMilliseconds,
RelativePhaseTimingInMs,

View file

@ -15,8 +15,11 @@ export interface DataAllocationMetaFields {
export interface MinAgeField {
minAgeUnit?: string;
minAgeToMilliSeconds: number;
}
export type MinAgePhase = 'warm' | 'cold' | 'frozen' | 'delete';
export interface ForcemergeFields {
bestCompression: boolean;
}

View file

@ -22,18 +22,25 @@ export function IndexLifecycleManagementPageProvider({ getService }: FtrProvider
policyName: string,
warmEnabled: boolean = false,
coldEnabled: boolean = false,
deletePhaseEnabled: boolean = false
deletePhaseEnabled: boolean = false,
minAges: { [key: string]: { value: string; unit: string } } = {
warm: { value: '10', unit: 'd' },
cold: { value: '15', unit: 'd' },
frozen: { value: '20', unit: 'd' },
}
) {
await testSubjects.setValue('policyNameField', policyName);
if (warmEnabled) {
await retry.try(async () => {
await testSubjects.click('enablePhaseSwitch-warm');
});
await testSubjects.setValue('warm-selectedMinimumAge', minAges.warm.value);
}
if (coldEnabled) {
await retry.try(async () => {
await testSubjects.click('enablePhaseSwitch-cold');
});
await testSubjects.setValue('cold-selectedMinimumAge', minAges.cold.value);
}
if (deletePhaseEnabled) {
await retry.try(async () => {
@ -48,10 +55,17 @@ export function IndexLifecycleManagementPageProvider({ getService }: FtrProvider
policyName: string,
warmEnabled: boolean = false,
coldEnabled: boolean = false,
deletePhaseEnabled: boolean = false
deletePhaseEnabled: boolean = false,
minAges?: { [key: string]: { value: string; unit: string } }
) {
await testSubjects.click('createPolicyButton');
await this.fillNewPolicyForm(policyName, warmEnabled, coldEnabled, deletePhaseEnabled);
await this.fillNewPolicyForm(
policyName,
warmEnabled,
coldEnabled,
deletePhaseEnabled,
minAges
);
await this.saveNewPolicy();
},