mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ILM] Add UI validation for min age value (#96718)
This commit is contained in:
parent
8e9ca66520
commit
67e512fe27
19 changed files with 341 additions and 47 deletions
|
@ -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());
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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' },
|
||||
},
|
||||
|
|
|
@ -25,9 +25,10 @@ const i18nTexts = {
|
|||
export const FormErrorsCallout: FunctionComponent = () => {
|
||||
const {
|
||||
errors: { hasErrors },
|
||||
isFormSubmitted,
|
||||
} = useFormErrorsContext();
|
||||
|
||||
if (!hasErrors) {
|
||||
if (!isFormSubmitted || !hasErrors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export {
|
||||
calculateRelativeFromAbsoluteMilliseconds,
|
||||
formDataToAbsoluteTimings,
|
||||
getPhaseMinAgeInMilliseconds,
|
||||
AbsoluteTimings,
|
||||
PhaseAgeInMilliseconds,
|
||||
RelativePhaseTimingInMs,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue