[ILM] Migrate Hot phase to Form Lib (#80012)

* wip

* added missing shared_imports file to index

* initial migration of hot phase to form lib

- tests are now broken
- need to break up the hot_phase file in to meaningful parts
- duplicated set_priority and forcemerge components

* Big refactor

- moved a lot of files around
- removed the need for the state to track whether rollover is set

* Integrate form lib serialization with existing serialization

- refactor serializePolicy -> legacySerializePolicy
- updated serialization of form lib to factor in pre-existing
  policy values. These should be interacted with in a non-lossy
  way.

* wip on fixing jest tests and some other refactors

* fix jest tests and other refactors

* delete existing hot phase serialization and tests

* beginning of serializer test for hot phase

* added serialization tests for form lib components

* fix some i18n issues

* fixed delete phase integration test

* move hot phase serialization test to pre-existing test location

* fix another jest test issue

* fix ui metric tracking for setting input priority in hot phase

* refactor use rollover switch to form lib component and update validation for number segments in force merge

* readded missing validation 🤦🏼‍♂️

* fix type check issues and setting of rollover enabled 🙄

* migrate all form lib components to spreading all rest props in EuiFormRow

* added comment to test helper function

* refactor test helper setPhaseIndexPriorityFormLib -> setPhaseIndexPriority

* refactor to use form schema

* Removed use of UseMultiFields component

- also fix missing "key" on react component in unrelated file
- fixed ordering of JSON in test file
- also removed default value from form schema so that when a
  value is not set for max size, max docs or max age it will
  remain unset in future policies

* update json flyout behaviour

* fix json policy serialization

* Fix type and i18n issues

* do not use form.subscribe

* add missing key value in cells

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2020-10-19 17:34:40 +02:00 committed by GitHub
parent 668a4d4366
commit 30fc4bed7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1908 additions and 1303 deletions

View file

@ -39,8 +39,8 @@ export const CheckBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) =>
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiCheckbox
label={field.label}

View file

@ -87,8 +87,8 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) =>
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiComboBox
noSuggestions

View file

@ -39,8 +39,8 @@ export const MultiSelectField = ({ field, euiFieldProps = {}, ...rest }: Props)
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiSelectable
allowExclusions={false}

View file

@ -39,8 +39,8 @@ export const NumericField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiFieldNumber
isInvalid={isInvalid}

View file

@ -39,8 +39,8 @@ export const RadioGroupField = ({ field, euiFieldProps = {}, ...rest }: Props) =
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiRadioGroup
idSelected={field.value as string}

View file

@ -50,8 +50,8 @@ export const RangeField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiRange
value={field.value as number}

View file

@ -44,8 +44,8 @@ export const SelectField = ({ field, euiFieldProps, ...rest }: Props) => {
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiSelect
fullWidth

View file

@ -42,8 +42,8 @@ export const SuperSelectField = ({ field, euiFieldProps = { options: [] }, ...re
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiSuperSelect
fullWidth

View file

@ -39,8 +39,8 @@ export const TextAreaField = ({ field, euiFieldProps = {}, ...rest }: Props) =>
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiTextArea
isInvalid={isInvalid}

View file

@ -39,8 +39,8 @@ export const TextField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiFieldText
isInvalid={isInvalid}

View file

@ -46,8 +46,8 @@ export const ToggleField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
error={errorMessage}
isInvalid={isInvalid}
fullWidth
data-test-subj={rest['data-test-subj']}
describedByIds={rest.idAria ? [rest.idAria] : undefined}
{...rest}
>
<EuiSwitch
label={field.label}

View file

@ -552,7 +552,9 @@ exports[`extend index management ilm summary extension should return extension w
2018-12-07 13:02:55
</dd>
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
key="phaseDefinition_title"
>
<dt
className="euiDescriptionList__title"
>

View file

@ -10,6 +10,23 @@ export const POLICY_NAME = 'my_policy';
export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy';
export const NEW_SNAPSHOT_POLICY_NAME = 'my_new_snapshot_policy';
export const DEFAULT_POLICY: PolicyFromES = {
version: 1,
modified_date: Date.now().toString(),
policy: {
name: '',
phases: {
hot: {
min_age: '123ms',
actions: {
rollover: {},
},
},
},
},
name: '',
};
export const DELETE_PHASE_POLICY: PolicyFromES = {
version: 1,
modified_date: Date.now().toString(),
@ -19,8 +36,12 @@ export const DELETE_PHASE_POLICY: PolicyFromES = {
min_age: '0ms',
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
},
},
delete: {

View file

@ -7,7 +7,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils';
import { registerTestBed, TestBedConfig } from '../../../../../test_utils';
import { POLICY_NAME } from './constants';
import { TestSubjects } from '../helpers';
@ -43,39 +43,110 @@ const testBedConfig: TestBedConfig = {
},
};
const initTestBed = registerTestBed(EditPolicy, testBedConfig);
const initTestBed = registerTestBed<TestSubjects>(EditPolicy, testBedConfig);
export interface EditPolicyTestBed extends TestBed<TestSubjects> {
actions: {
setWaitForSnapshotPolicy: (snapshotPolicyName: string) => void;
savePolicy: () => void;
};
}
type SetupReturn = ReturnType<typeof setup>;
export const setup = async (): Promise<EditPolicyTestBed> => {
export type EditPolicyTestBed = SetupReturn extends Promise<infer U> ? U : SetupReturn;
export const setup = async () => {
const testBed = await initTestBed();
const { find, component } = testBed;
const setWaitForSnapshotPolicy = async (snapshotPolicyName: string) => {
const { component } = testBed;
act(() => {
testBed.find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]);
find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]);
});
component.update();
};
const savePolicy = async () => {
const { component, find } = testBed;
await act(async () => {
find('savePolicyButton').simulate('click');
});
component.update();
};
const toggleRollover = async (checked: boolean) => {
await act(async () => {
find('rolloverSwitch').simulate('click', { target: { checked } });
});
component.update();
};
const setMaxSize = async (value: string, units?: string) => {
await act(async () => {
find('hot-selectedMaxSizeStored').simulate('change', { target: { value } });
if (units) {
find('hot-selectedMaxSizeStoredUnits.select').simulate('change', {
target: { value: units },
});
}
});
component.update();
};
const setMaxDocs = async (value: string) => {
await act(async () => {
find('hot-selectedMaxDocuments').simulate('change', { target: { value } });
});
component.update();
};
const setMaxAge = async (value: string, units?: string) => {
await act(async () => {
find('hot-selectedMaxAge').simulate('change', { target: { value } });
if (units) {
find('hot-selectedMaxAgeUnits.select').simulate('change', { target: { value: units } });
}
});
component.update();
};
const toggleForceMerge = (phase: string) => async (checked: boolean) => {
await act(async () => {
find(`${phase}-forceMergeSwitch`).simulate('click', { target: { checked } });
});
component.update();
};
const setForcemergeSegmentsCount = (phase: string) => async (value: string) => {
await act(async () => {
find(`${phase}-selectedForceMergeSegments`).simulate('change', { target: { value } });
});
component.update();
};
const setBestCompression = (phase: string) => async (checked: boolean) => {
await act(async () => {
find(`${phase}-bestCompression`).simulate('click', { target: { checked } });
});
component.update();
};
const setIndexPriority = (phase: string) => async (value: string) => {
await act(async () => {
find(`${phase}-phaseIndexPriority`).simulate('change', { target: { value } });
});
component.update();
};
return {
...testBed,
actions: {
setWaitForSnapshotPolicy,
savePolicy,
hot: {
setMaxSize,
setMaxDocs,
setMaxAge,
toggleRollover,
toggleForceMerge: toggleForceMerge('hot'),
setForcemergeSegments: setForcemergeSegmentsCount('hot'),
setBestCompression: setBestCompression('hot'),
setIndexPriority: setIndexPriority('hot'),
},
},
};
};

View file

@ -10,7 +10,12 @@ import { setupEnvironment } from '../helpers/setup_environment';
import { EditPolicyTestBed, setup } from './edit_policy.helpers';
import { API_BASE_PATH } from '../../../common/constants';
import { DELETE_PHASE_POLICY, NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME } from './constants';
import {
DELETE_PHASE_POLICY,
NEW_SNAPSHOT_POLICY_NAME,
SNAPSHOT_POLICY_NAME,
DEFAULT_POLICY,
} from './constants';
window.scrollTo = jest.fn();
@ -21,6 +26,83 @@ describe('<EditPolicy />', () => {
server.restore();
});
describe('hot phase', () => {
describe('serialization', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]);
httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
await act(async () => {
testBed = await setup();
});
const { component } = testBed;
component.update();
});
test('setting all values', async () => {
const { actions } = testBed;
await actions.hot.setMaxSize('123', 'mb');
await actions.hot.setMaxDocs('123');
await actions.hot.setMaxAge('123', 'h');
await actions.hot.toggleForceMerge(true);
await actions.hot.setForcemergeSegments('123');
await actions.hot.setBestCompression(true);
await actions.hot.setIndexPriority('123');
await actions.savePolicy();
const latestRequest = server.requests[server.requests.length - 1];
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toMatchInlineSnapshot(`
Object {
"name": "my_policy",
"phases": Object {
"hot": Object {
"actions": Object {
"forcemerge": Object {
"index_codec": "best_compression",
"max_num_segments": 123,
},
"rollover": Object {
"max_age": "123h",
"max_docs": 123,
"max_size": "123mb",
},
"set_priority": Object {
"priority": 123,
},
},
"min_age": "0ms",
},
},
}
`);
});
test('disabling rollover', async () => {
const { actions } = testBed;
await actions.hot.toggleRollover(false);
await actions.savePolicy();
const latestRequest = server.requests[server.requests.length - 1];
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toMatchInlineSnapshot(`
Object {
"name": "my_policy",
"phases": Object {
"hot": Object {
"actions": Object {
"set_priority": Object {
"priority": 100,
},
},
"min_age": "0ms",
},
},
}
`);
});
});
});
describe('delete phase', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadPolicies([DELETE_PHASE_POLICY]);

View file

@ -9,4 +9,12 @@ export type TestSubjects =
| 'savePolicyButton'
| 'customPolicyCallout'
| 'noPoliciesCallout'
| 'policiesErrorCallout';
| 'policiesErrorCallout'
| 'rolloverSwitch'
| 'rolloverSettingsRequired'
| 'hot-selectedMaxSizeStored'
| 'hot-selectedMaxSizeStoredUnits'
| 'hot-selectedMaxDocuments'
| 'hot-selectedMaxAge'
| 'hot-selectedMaxAgeUnits'
| string;

View file

@ -33,15 +33,12 @@ import {
positiveNumbersAboveZeroErrorMessage,
positiveNumberRequiredMessage,
numberRequiredMessage,
maximumAgeRequiredMessage,
maximumSizeRequiredMessage,
policyNameRequiredMessage,
policyNameStartsWithUnderscoreErrorMessage,
policyNameContainsCommaErrorMessage,
policyNameContainsSpaceErrorMessage,
policyNameMustBeDifferentErrorMessage,
policyNameAlreadyUsedErrorMessage,
maximumDocumentsRequiredMessage,
} from '../../public/application/services/policies/policy_validation';
import { editPolicyHelpers } from './helpers';
@ -116,8 +113,10 @@ const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[
expect(foundErrorMessage).toBe(true);
});
};
const noRollover = (rendered: ReactWrapper) => {
findTestSubject(rendered, 'rolloverSwitch').simulate('click');
const noRollover = async (rendered: ReactWrapper) => {
await act(async () => {
findTestSubject(rendered, 'rolloverSwitch').simulate('click');
});
rendered.update();
};
const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => {
@ -133,7 +132,7 @@ const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string | nu
afterInput.simulate('change', { target: { value: after } });
rendered.update();
};
const setPhaseIndexPriority = (
const setPhaseIndexPriorityLegacy = (
rendered: ReactWrapper,
phase: string,
priority: string | number
@ -142,12 +141,43 @@ const setPhaseIndexPriority = (
priorityInput.simulate('change', { target: { value: priority } });
rendered.update();
};
const save = (rendered: ReactWrapper) => {
const setPhaseIndexPriority = async (
rendered: ReactWrapper,
phase: string,
priority: string | number
) => {
const priorityInput = findTestSubject(rendered, `${phase}-phaseIndexPriority`);
await act(async () => {
priorityInput.simulate('change', { target: { value: priority } });
});
rendered.update();
};
const save = async (rendered: ReactWrapper) => {
const saveButton = findTestSubject(rendered, 'savePolicyButton');
saveButton.simulate('click');
await act(async () => {
saveButton.simulate('click');
});
rendered.update();
};
describe('edit policy', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
/**
* The form lib has a short delay (setTimeout) before running and rendering
* any validation errors. This helper advances timers and can trigger component
* state changes.
*/
const waitForFormLibValidation = () => {
act(() => {
jest.advanceTimersByTime(1000);
});
};
beforeEach(() => {
component = (
<KibanaContextProvider services={{ cloud: { isCloudEnabled: false } as CloudSetup }}>
@ -166,27 +196,27 @@ describe('edit policy', () => {
httpRequestsMockHelpers.setPoliciesResponse(policies);
});
describe('top level form', () => {
test('should show error when trying to save empty form', () => {
test('should show error when trying to save empty form', async () => {
const rendered = mountWithIntl(component);
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [policyNameRequiredMessage]);
});
test('should show error when trying to save policy name with space', () => {
test('should show error when trying to save policy name with space', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'my policy');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [policyNameContainsSpaceErrorMessage]);
});
test('should show error when trying to save policy name that is already used', () => {
test('should show error when trying to save policy name that is already used', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'testy0');
rendered.update();
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [policyNameAlreadyUsedErrorMessage]);
});
test('should show error when trying to save as new policy but using the same name', () => {
test('should show error when trying to save as new policy but using the same name', async () => {
component = (
<EditPolicy
policyName={'testy0'}
@ -199,42 +229,46 @@ describe('edit policy', () => {
findTestSubject(rendered, 'saveAsNewSwitch').simulate('click');
rendered.update();
setPolicyName(rendered, 'testy0');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [policyNameMustBeDifferentErrorMessage]);
});
test('should show error when trying to save policy name with comma', () => {
test('should show error when trying to save policy name with comma', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'my,policy');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [policyNameContainsCommaErrorMessage]);
});
test('should show error when trying to save policy name starting with underscore', () => {
test('should show error when trying to save policy name starting with underscore', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, '_mypolicy');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [policyNameStartsWithUnderscoreErrorMessage]);
});
test('should show correct json in policy flyout', () => {
test('should show correct json in policy flyout', async () => {
const rendered = mountWithIntl(component);
findTestSubject(rendered, 'requestButton').simulate('click');
await act(async () => {
findTestSubject(rendered, 'requestButton').simulate('click');
});
rendered.update();
const json = rendered.find(`code`).text();
const expected = `PUT _ilm/policy/<policyName>\n${JSON.stringify(
{
policy: {
phases: {
hot: {
min_age: '0ms',
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
rollover: {
max_size: '50gb',
max_age: '30d',
},
},
min_age: '0ms',
},
},
},
@ -246,55 +280,66 @@ describe('edit policy', () => {
});
});
describe('hot phase', () => {
test('should show errors when trying to save with no max size and no max age', () => {
test('should show errors when trying to save with no max size and no max age', async () => {
const rendered = mountWithIntl(component);
expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy();
setPolicyName(rendered, 'mypolicy');
const maxSizeInput = rendered.find(`input#hot-selectedMaxSizeStored`);
maxSizeInput.simulate('change', { target: { value: '' } });
const maxAgeInput = rendered.find(`input#hot-selectedMaxAge`);
maxAgeInput.simulate('change', { target: { value: '' } });
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '' } });
});
waitForFormLibValidation();
const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge');
await act(async () => {
maxAgeInput.simulate('change', { target: { value: '' } });
});
waitForFormLibValidation();
rendered.update();
save(rendered);
expectedErrorMessages(rendered, [
maximumSizeRequiredMessage,
maximumAgeRequiredMessage,
maximumDocumentsRequiredMessage,
]);
await save(rendered);
expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeTruthy();
});
test('should show number above 0 required error when trying to save with -1 for max size', () => {
test('should show number above 0 required error when trying to save with -1 for max size', async () => {
const rendered = mountWithIntl(component);
setPolicyName(rendered, 'mypolicy');
const maxSizeInput = rendered.find(`input#hot-selectedMaxSizeStored`);
maxSizeInput.simulate('change', { target: { value: -1 } });
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '-1' } });
});
waitForFormLibValidation();
rendered.update();
save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show number above 0 required error when trying to save with 0 for max size', () => {
test('should show number above 0 required error when trying to save with 0 for max size', async () => {
const rendered = mountWithIntl(component);
setPolicyName(rendered, 'mypolicy');
const maxSizeInput = rendered.find(`input#hot-selectedMaxSizeStored`);
maxSizeInput.simulate('change', { target: { value: 0 } });
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '-1' } });
});
waitForFormLibValidation();
rendered.update();
save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show number above 0 required error when trying to save with -1 for max age', () => {
test('should show number above 0 required error when trying to save with -1 for max age', async () => {
const rendered = mountWithIntl(component);
setPolicyName(rendered, 'mypolicy');
const maxSizeInput = rendered.find(`input#hot-selectedMaxAge`);
maxSizeInput.simulate('change', { target: { value: -1 } });
const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge');
await act(async () => {
maxAgeInput.simulate('change', { target: { value: '-1' } });
});
waitForFormLibValidation();
rendered.update();
save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show number above 0 required error when trying to save with 0 for max age', () => {
test('should show number above 0 required error when trying to save with 0 for max age', async () => {
const rendered = mountWithIntl(component);
setPolicyName(rendered, 'mypolicy');
const maxSizeInput = rendered.find(`input#hot-selectedMaxAge`);
maxSizeInput.simulate('change', { target: { value: 0 } });
const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge');
await act(async () => {
maxAgeInput.simulate('change', { target: { value: '0' } });
});
waitForFormLibValidation();
rendered.update();
save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show forcemerge input when rollover enabled', () => {
@ -302,22 +347,27 @@ describe('edit policy', () => {
setPolicyName(rendered, 'mypolicy');
expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeTruthy();
});
test('should hide forcemerge input when rollover is disabled', () => {
test('should hide forcemerge input when rollover is disabled', async () => {
const rendered = mountWithIntl(component);
setPolicyName(rendered, 'mypolicy');
noRollover(rendered);
await noRollover(rendered);
waitForFormLibValidation();
rendered.update();
expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy();
});
test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => {
const rendered = mountWithIntl(component);
setPolicyName(rendered, 'mypolicy');
findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click');
act(() => {
findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click');
});
rendered.update();
const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments');
forcemergeInput.simulate('change', { target: { value: '0' } });
await act(async () => {
forcemergeInput.simulate('change', { target: { value: '0' } });
});
waitForFormLibValidation();
rendered.update();
save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => {
@ -326,18 +376,22 @@ describe('edit policy', () => {
findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click');
rendered.update();
const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments');
forcemergeInput.simulate('change', { target: { value: '-1' } });
await act(async () => {
forcemergeInput.simulate('change', { target: { value: '-1' } });
});
waitForFormLibValidation();
rendered.update();
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show positive number required error when trying to save with -1 for index priority', () => {
test('should show positive number required error when trying to save with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
setPhaseIndexPriority(rendered, 'hot', '-1');
save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
await setPhaseIndexPriority(rendered, 'hot', '-1');
waitForFormLibValidation();
rendered.update();
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
});
describe('warm phase', () => {
@ -351,44 +405,44 @@ describe('edit policy', () => {
test('should show number required error when trying to save empty warm phase', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', '');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [numberRequiredMessage]);
});
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', '0');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, []);
});
test('should show positive number required error when trying to save warm phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', '-1');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', '1');
setPhaseIndexPriority(rendered, 'warm', '-1');
save(rendered);
setPhaseIndexPriorityLegacy(rendered, 'warm', '-1');
await save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
findTestSubject(rendered, 'shrinkSwitch').simulate('click');
@ -397,12 +451,12 @@ describe('edit policy', () => {
const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount');
shrinkInput.simulate('change', { target: { value: '0' } });
rendered.update();
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', '1');
@ -411,12 +465,12 @@ describe('edit policy', () => {
const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount');
shrinkInput.simulate('change', { target: { value: '-1' } });
rendered.update();
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', '1');
@ -425,12 +479,12 @@ describe('edit policy', () => {
const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments');
forcemergeInput.simulate('change', { target: { value: '0' } });
rendered.update();
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', '1');
@ -439,13 +493,13 @@ describe('edit policy', () => {
const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments');
forcemergeInput.simulate('change', { target: { value: '-1' } });
rendered.update();
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
test('should show spinner for node attributes input when loading', async () => {
server.respondImmediately = false;
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
@ -459,7 +513,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -469,7 +523,7 @@ describe('edit policy', () => {
});
test('should show node attributes input when attributes exist', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -481,7 +535,7 @@ describe('edit policy', () => {
});
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -508,7 +562,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -521,7 +575,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -534,7 +588,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -551,26 +605,26 @@ describe('edit policy', () => {
});
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
setPhaseAfter(rendered, 'cold', '0');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, []);
});
test('should show positive number required error when trying to save cold phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
setPhaseAfter(rendered, 'cold', '-1');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
test('should show spinner for node attributes input when loading', async () => {
server.respondImmediately = false;
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
@ -584,7 +638,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -594,7 +648,7 @@ describe('edit policy', () => {
});
test('should show node attributes input when attributes exist', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -606,7 +660,7 @@ describe('edit policy', () => {
});
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -628,12 +682,12 @@ describe('edit policy', () => {
});
test('should show positive number required error when trying to save with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
setPhaseAfter(rendered, 'cold', '1');
setPhaseIndexPriority(rendered, 'cold', '-1');
save(rendered);
setPhaseIndexPriorityLegacy(rendered, 'cold', '-1');
await save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
test('should show default allocation warning when no node roles are found', async () => {
@ -643,7 +697,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -656,7 +710,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -669,7 +723,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -679,20 +733,20 @@ describe('edit policy', () => {
describe('delete phase', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'delete');
setPhaseAfter(rendered, 'delete', '0');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, []);
});
test('should show positive number required error when trying to save delete phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'delete');
setPhaseAfter(rendered, 'delete', '-1');
save(rendered);
await save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
});
@ -707,7 +761,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: true,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -746,7 +800,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: true,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -767,7 +821,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -785,7 +839,7 @@ describe('edit policy', () => {
isUsingDeprecatedDataRoleConfig: false,
});
const rendered = mountWithIntl(component);
noRollover(rendered);
await noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();

View file

@ -107,10 +107,9 @@ export interface ForcemergeAction {
index_codec?: 'best_compression';
}
export interface Policy {
export interface LegacyPolicy {
name: string;
phases: {
hot: HotPhase;
warm: WarmPhase;
cold: ColdPhase;
delete: DeletePhase;
@ -155,18 +154,6 @@ export interface PhaseWithForcemergeAction {
bestCompressionEnabled: boolean;
}
export interface HotPhase
extends CommonPhaseSettings,
PhaseWithIndexPriority,
PhaseWithForcemergeAction {
rolloverEnabled: boolean;
selectedMaxSizeStored: string;
selectedMaxSizeStoredUnits: string;
selectedMaxDocuments: string;
selectedMaxAge: string;
selectedMaxAgeUnits: string;
}
export interface WarmPhase
extends CommonPhaseSettings,
PhaseWithMinAge,

View file

@ -5,4 +5,5 @@
*/
export * from './policy';
export * from './ui_metric';

View file

@ -8,22 +8,24 @@ import {
SerializedPhase,
ColdPhase,
DeletePhase,
HotPhase,
WarmPhase,
SerializedPolicy,
} from '../../../common/types';
export const defaultNewHotPhase: HotPhase = {
phaseEnabled: true,
rolloverEnabled: true,
selectedMaxAge: '30',
selectedMaxAgeUnits: 'd',
selectedMaxSizeStored: '50',
selectedMaxSizeStoredUnits: 'gb',
forceMergeEnabled: false,
selectedForceMergeSegments: '',
bestCompressionEnabled: false,
phaseIndexPriority: '100',
selectedMaxDocuments: '',
export const defaultSetPriority: string = '100';
export const defaultPolicy: SerializedPolicy = {
name: '',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
},
},
},
};
export const defaultNewWarmPhase: WarmPhase = {

View file

@ -4,6 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
/**
* PLEASE NOTE: This component is currently duplicated. A version of this component wired up with
* the form lib lives in ./phases/shared
*/
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiDescribedFormGroup,

View file

@ -9,7 +9,7 @@ import { EuiFormRow, EuiFormRowProps } from '@elastic/eui';
type Props = EuiFormRowProps & {
isShowingErrors: boolean;
errors?: string[];
errors?: string | string[] | null;
};
export const ErrableFormRow: React.FunctionComponent<Props> = ({
@ -18,8 +18,13 @@ export const ErrableFormRow: React.FunctionComponent<Props> = ({
children,
...rest
}) => {
const _errors = errors ? (Array.isArray(errors) ? errors : [errors]) : undefined;
return (
<EuiFormRow isInvalid={errors && isShowingErrors && errors.length > 0} error={errors} {...rest}>
<EuiFormRow
isInvalid={isShowingErrors || (_errors && _errors.length > 0)}
error={errors}
{...rest}
>
<Fragment>
{Children.map(children, (child) =>
cloneElement(child as ReactElement, {

View file

@ -22,3 +22,5 @@ export {
} from './data_tier_allocation';
export { DescribedFormField } from './described_form_field';
export { Forcemerge } from './forcemerge';
export * from './phases';

View file

@ -212,7 +212,7 @@ export const MinAgeInput = <Phase extends PhaseWithMinAge>({
<EuiFieldNumber
id={`${phase}-${selectedMinimumAgeProperty}`}
value={phaseData.selectedMinimumAge}
onChange={async (e) => {
onChange={(e) => {
setPhaseData(selectedMinimumAgeProperty, e.target.value);
}}
min={0}

View file

@ -10,8 +10,11 @@ import { i18n } from '@kbn/i18n';
import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui';
import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../common/types';
import { PhaseValidationErrors } from '../../../services/policies/policy_validation';
import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../../common/types';
import { useFormData } from '../../../../../shared_imports';
import { PhaseValidationErrors } from '../../../../services/policies/policy_validation';
import {
LearnMoreLink,
@ -22,9 +25,9 @@ import {
SetPriorityInput,
MinAgeInput,
DescribedFormField,
} from '../components';
} from '../';
import { DataTierAllocationField } from './shared';
import { DataTierAllocationField, useRolloverPath } from './shared';
const i18nTexts = {
freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', {
@ -46,15 +49,17 @@ interface Props {
phaseData: ColdPhaseInterface;
isShowingErrors: boolean;
errors?: PhaseValidationErrors<ColdPhaseInterface>;
hotPhaseRolloverEnabled: boolean;
}
export const ColdPhase: FunctionComponent<Props> = ({
setPhaseData,
phaseData,
errors,
isShowingErrors,
hotPhaseRolloverEnabled,
}) => {
const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
watch: [useRolloverPath],
});
return (
<div id="coldPhaseContent" aria-live="polite" role="region">
<>

View file

@ -0,0 +1,155 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
import { DeletePhase as DeletePhaseInterface, Phases } from '../../../../../../common/types';
import { useFormData } from '../../../../../shared_imports';
import { PhaseValidationErrors } from '../../../../services/policies/policy_validation';
import {
ActiveBadge,
LearnMoreLink,
OptionalLabel,
PhaseErrorMessage,
MinAgeInput,
SnapshotPolicies,
} from '../';
import { useRolloverPath } from './shared';
const deleteProperty: keyof Phases = 'delete';
const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName;
interface Props {
setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void;
phaseData: DeletePhaseInterface;
isShowingErrors: boolean;
errors?: PhaseValidationErrors<DeletePhaseInterface>;
getUrlForApp: (
appId: string,
options?: {
path?: string;
absolute?: boolean;
}
) => string;
}
export const DeletePhase: FunctionComponent<Props> = ({
setPhaseData,
phaseData,
errors,
isShowingErrors,
getUrlForApp,
}) => {
const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
watch: [useRolloverPath],
});
return (
<div id="deletePhaseContent" aria-live="polite" role="region">
<EuiDescribedFormGroup
title={
<div>
<h2 className="eui-displayInlineBlock eui-alignMiddle">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseLabel"
defaultMessage="Delete phase"
/>
</h2>{' '}
{phaseData.phaseEnabled && !isShowingErrors ? <ActiveBadge /> : null}
<PhaseErrorMessage isShowingErrors={isShowingErrors} />
</div>
}
titleSize="s"
description={
<Fragment>
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescriptionText"
defaultMessage="You no longer need your index. You can define when it is safe to delete it."
/>
</p>
<EuiSwitch
data-test-subj="enablePhaseSwitch-delete"
label={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.activateWarmPhaseSwitchLabel"
defaultMessage="Activate delete phase"
/>
}
id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`}
checked={phaseData.phaseEnabled}
onChange={(e) => {
setPhaseData(phaseProperty('phaseEnabled'), e.target.checked);
}}
aria-controls="deletePhaseContent"
/>
</Fragment>
}
fullWidth
>
{phaseData.phaseEnabled ? (
<MinAgeInput<DeletePhaseInterface>
errors={errors}
phaseData={phaseData}
phase={deleteProperty}
isShowingErrors={isShowingErrors}
setPhaseData={setPhaseData}
rolloverEnabled={hotPhaseRolloverEnabled}
/>
) : (
<div />
)}
</EuiDescribedFormGroup>
{phaseData.phaseEnabled ? (
<EuiDescribedFormGroup
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.waitForSnapshotTitle"
defaultMessage="Wait for snapshot policy"
/>
</h3>
}
description={
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.waitForSnapshotDescription"
defaultMessage="Specify a snapshot policy to be executed before the deletion of the index. This ensures that a snapshot of the deleted index is available."
/>{' '}
<LearnMoreLink docPath="ilm-wait-for-snapshot.html" />
</EuiTextColor>
}
titleSize="xs"
fullWidth
>
<EuiFormRow
id="deletePhaseWaitForSnapshot"
label={
<Fragment>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.waitForSnapshotLabel"
defaultMessage="Snapshot policy name"
/>
<OptionalLabel />
</Fragment>
}
>
<SnapshotPolicies
value={phaseData.waitForSnapshotPolicy}
onChange={(value) => setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)}
getUrlForApp={getUrlForApp}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
) : null}
</div>
);
};

View file

@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const maxSizeStoredUnits = [
{
value: 'gb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel', {
defaultMessage: 'gigabytes',
}),
},
{
value: 'mb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.megabytesLabel', {
defaultMessage: 'megabytes',
}),
},
{
value: 'b',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.bytesLabel', {
defaultMessage: 'bytes',
}),
},
{
value: 'kb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.kilobytesLabel', {
defaultMessage: 'kilobytes',
}),
},
{
value: 'tb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.terabytesLabel', {
defaultMessage: 'terabytes',
}),
},
{
value: 'pb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.petabytesLabel', {
defaultMessage: 'petabytes',
}),
},
];
export const maxAgeUnits = [
{
value: 'd',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.daysLabel', {
defaultMessage: 'days',
}),
},
{
value: 'h',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.hoursLabel', {
defaultMessage: 'hours',
}),
},
{
value: 'm',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.minutesLabel', {
defaultMessage: 'minutes',
}),
},
{
value: 's',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.secondsLabel', {
defaultMessage: 'seconds',
}),
},
{
value: 'ms',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.millisecondsLabel', {
defaultMessage: 'milliseconds',
}),
},
{
value: 'micros',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.microsecondsLabel', {
defaultMessage: 'microseconds',
}),
},
{
value: 'nanos',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.nanosecondsLabel', {
defaultMessage: 'nanoseconds',
}),
},
];

View file

@ -0,0 +1,235 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FunctionComponent, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiDescribedFormGroup,
EuiCallOut,
} from '@elastic/eui';
import { Phases } from '../../../../../../../common/types';
import {
useFormContext,
useFormData,
UseField,
SelectField,
ToggleField,
NumericField,
} from '../../../../../../shared_imports';
import { ROLLOVER_EMPTY_VALIDATION } from '../../../form_validations';
import { ROLLOVER_FORM_PATHS } from '../../../constants';
import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../';
import { Forcemerge, SetPriorityInput } from '../shared';
import { maxSizeStoredUnits, maxAgeUnits } from './constants';
import { i18nTexts } from './i18n_texts';
import { useRolloverPath } from '../shared';
const hotProperty: keyof Phases = 'hot';
export const HotPhase: FunctionComponent<{ setWarmPhaseOnRollover: (v: boolean) => void }> = ({
setWarmPhaseOnRollover,
}) => {
const [{ [useRolloverPath]: isRolloverEnabled }] = useFormData({ watch: [useRolloverPath] });
const form = useFormContext();
const isShowingErrors = form.isValid === false;
const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false);
useEffect(() => {
setWarmPhaseOnRollover(isRolloverEnabled ?? false);
}, [setWarmPhaseOnRollover, isRolloverEnabled]);
return (
<>
<EuiDescribedFormGroup
title={
<div>
<h2 className="eui-displayInlineBlock eui-alignMiddle">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseLabel"
defaultMessage="Hot phase"
/>
</h2>{' '}
{isShowingErrors ? null : <ActiveBadge />}
<PhaseErrorMessage isShowingErrors={isShowingErrors} />
</div>
}
titleSize="s"
description={
<Fragment>
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescriptionMessage"
defaultMessage="This phase is required. You are actively querying and
writing to your index. For faster updates, you can roll over the index when it gets too big or too old."
/>
</p>
</Fragment>
}
fullWidth
>
<UseField<boolean>
key="_meta.hot.useRollover"
path="_meta.hot.useRollover"
component={ToggleField}
componentProps={{
hasEmptyLabelSpace: true,
fullWidth: false,
helpText: (
<>
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDescriptionMessage"
defaultMessage="The new index created by rollover is added
to the index alias and designated as the write index."
/>
</p>
<LearnMoreLink
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hotPhase.learnAboutRolloverLinkText"
defaultMessage="Learn about rollover"
/>
}
docPath="indices-rollover-index.html"
/>
<EuiSpacer size="m" />
</>
),
euiFieldProps: {
'data-test-subj': 'rolloverSwitch',
},
}}
/>
{isRolloverEnabled && (
<>
<EuiSpacer size="m" />
{showEmptyRolloverFieldsError && (
<>
<EuiCallOut
title={i18nTexts.rollOverConfigurationCallout.title}
data-test-subj="rolloverSettingsRequired"
color="danger"
>
<div>{i18nTexts.rollOverConfigurationCallout.body}</div>
</EuiCallOut>
<EuiSpacer size="s" />
</>
)}
<EuiFlexGroup>
<EuiFlexItem style={{ maxWidth: 188 }}>
<UseField path={ROLLOVER_FORM_PATHS.maxSize}>
{(field) => {
const showErrorCallout = field.errors.some(
(e) => e.validationType === ROLLOVER_EMPTY_VALIDATION
);
if (showErrorCallout !== showEmptyRolloverFieldsError) {
setShowEmptyRolloverFieldsError(showErrorCallout);
}
return (
<NumericField
field={field}
euiFieldProps={{
'data-test-subj': `${hotProperty}-selectedMaxSizeStored`,
min: 1,
}}
/>
);
}}
</UseField>
</EuiFlexItem>
<EuiFlexItem style={{ maxWidth: 188 }}>
<UseField
key="_meta.hot.maxStorageSizeUnit"
path="_meta.hot.maxStorageSizeUnit"
component={SelectField}
componentProps={{
'data-test-subj': `${hotProperty}-selectedMaxSizeStoredUnits`,
hasEmptyLabelSpace: true,
euiFieldProps: {
options: maxSizeStoredUnits,
'aria-label': i18n.translate(
'xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeUnitsAriaLabel',
{
defaultMessage: 'Maximum index size units',
}
),
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem style={{ maxWidth: 188 }}>
<UseField
path={ROLLOVER_FORM_PATHS.maxDocs}
component={NumericField}
componentProps={{
euiFieldProps: {
'data-test-subj': `${hotProperty}-selectedMaxDocuments`,
min: 1,
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem style={{ maxWidth: 188 }}>
<UseField
path={ROLLOVER_FORM_PATHS.maxAge}
component={NumericField}
componentProps={{
euiFieldProps: {
'data-test-subj': `${hotProperty}-selectedMaxAge`,
min: 1,
},
}}
/>
</EuiFlexItem>
<EuiFlexItem style={{ maxWidth: 188 }}>
<UseField
key="_meta.hot.maxAgeUnit"
path="_meta.hot.maxAgeUnit"
component={SelectField}
componentProps={{
'data-test-subj': `${hotProperty}-selectedMaxAgeUnits`,
hasEmptyLabelSpace: true,
euiFieldProps: {
'aria-label': i18n.translate(
'xpack.indexLifecycleMgmt.hotPhase.maximumAgeUnitsAriaLabel',
{
defaultMessage: 'Maximum age units',
}
),
options: maxAgeUnits,
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</EuiDescribedFormGroup>
{isRolloverEnabled && <Forcemerge phase="hot" />}
<SetPriorityInput phase={hotProperty} />
</>
);
};

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const i18nTexts = {
maximumAgeRequiredMessage: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError',
{
defaultMessage: 'A maximum age is required.',
}
),
maximumSizeRequiredMessage: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError',
{
defaultMessage: 'A maximum index size is required.',
}
),
maximumDocumentsRequiredMessage: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError',
{
defaultMessage: 'Maximum documents is required.',
}
),
rollOverConfigurationCallout: {
title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.rolloverConfigurationError.title', {
defaultMessage: 'Invalid rollover configuration',
}),
body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.rolloverConfigurationError.body', {
defaultMessage:
'A value for one of maximum size, maximum documents, or maximum age is required.',
}),
},
};

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { DataTierAllocationField } from './data_tier_allocation_field';
export { HotPhase } from './hot_phase';

View file

@ -5,6 +5,9 @@
*/
export { HotPhase } from './hot_phase';
export { WarmPhase } from './warm_phase';
export { ColdPhase } from './cold_phase';
export { DeletePhase } from './delete_phase';

View file

@ -8,11 +8,11 @@ import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiDescribedFormGroup, EuiFormRow, EuiSpacer } from '@elastic/eui';
import { useKibana } from '../../../../../shared_imports';
import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../common/types';
import { PhaseValidationErrors } from '../../../../services/policies/policy_validation';
import { getAvailableNodeRoleForPhase } from '../../../../lib/data_tiers';
import { isNodeRoleFirstPreference } from '../../../../lib/data_tiers/is_node_role_first_preference';
import { useKibana } from '../../../../../../shared_imports';
import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../../common/types';
import { PhaseValidationErrors } from '../../../../../services/policies/policy_validation';
import { getAvailableNodeRoleForPhase } from '../../../../../lib/data_tiers';
import { isNodeRoleFirstPreference } from '../../../../../lib/data_tiers/is_node_role_first_preference';
import {
DataTierAllocation,
@ -20,7 +20,7 @@ import {
NoNodeAttributesWarning,
NodesDataProvider,
CloudDataTierCallout,
} from '../../components/data_tier_allocation';
} from '../../data_tier_allocation';
const i18nTexts = {
title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', {

View file

@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiDescribedFormGroup, EuiSpacer, EuiTextColor } from '@elastic/eui';
import React from 'react';
import { Phases } from '../../../../../../../common/types';
import { UseField, ToggleField, NumericField, useFormData } from '../../../../../../shared_imports';
import { i18nTexts } from '../../../i18n_texts';
import { LearnMoreLink } from '../../';
interface Props {
phase: keyof Phases & string;
}
const forceMergeEnabledPath = '_meta.hot.forceMergeEnabled';
export const Forcemerge: React.FunctionComponent<Props> = ({ phase }) => {
const [{ [forceMergeEnabledPath]: forceMergeEnabled }] = useFormData({
watch: [forceMergeEnabledPath],
});
return (
<EuiDescribedFormGroup
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableText"
defaultMessage="Force merge"
/>
</h3>
}
description={
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableExplanationText"
defaultMessage="Reduce the number of segments in your shard by merging smaller files and clearing deleted ones."
/>{' '}
<LearnMoreLink docPath="indices-forcemerge.html" />
</EuiTextColor>
}
titleSize="xs"
fullWidth
>
<UseField
key={forceMergeEnabledPath}
path={forceMergeEnabledPath}
component={ToggleField}
componentProps={{
euiFieldProps: {
'data-test-subj': `${phase}-forceMergeSwitch`,
'aria-label': i18nTexts.editPolicy.forceMergeEnabledFieldLabel,
'aria-controls': 'forcemergeContent',
},
}}
/>
<EuiSpacer />
<div id="forcemergeContent" aria-live="polite" role="region">
{forceMergeEnabled && (
<>
<UseField
key={`phases.${phase}.actions.forcemerge.max_num_segments`}
path={`phases.${phase}.actions.forcemerge.max_num_segments`}
component={NumericField}
componentProps={{
fullWidth: false,
euiFieldProps: {
'data-test-subj': `${phase}-selectedForceMergeSegments`,
min: 1,
},
}}
/>
<UseField
key="_meta.hot.bestCompression"
path="_meta.hot.bestCompression"
component={ToggleField}
componentProps={{
hasEmptyLabelSpace: true,
euiFieldProps: {
'data-test-subj': `${phase}-bestCompression`,
},
}}
/>
</>
)}
</div>
</EuiDescribedFormGroup>
);
};

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { useRolloverPath } from '../../../constants';
export { DataTierAllocationField } from './data_tier_allocation_field';
export { Forcemerge } from './forcemerge_field';
export { SetPriorityInput } from './set_priority_input';

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui';
import { PhaseWithIndexPriority, Phases } from '../../../../../../../common/types';
import { UseField, NumericField } from '../../../../../../shared_imports';
import { propertyof } from '../../../../../services/policies/policy_validation';
import { LearnMoreLink } from '../../';
interface Props {
phase: keyof Phases & string;
}
export const SetPriorityInput: FunctionComponent<Props> = ({ phase }) => {
const phaseIndexPriorityProperty = propertyof<PhaseWithIndexPriority>('phaseIndexPriority');
return (
<EuiDescribedFormGroup
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.indexPriorityText"
defaultMessage="Index priority"
/>
</h3>
}
description={
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.warmPhase.indexPriorityExplanationText"
defaultMessage="Set the priority for recovering your indices after a node restart.
Indices with higher priorities are recovered before indices with lower priorities."
/>{' '}
<LearnMoreLink docPath="recovery-prioritization.html" />
</EuiTextColor>
}
titleSize="xs"
fullWidth
>
<UseField
key={`phases.${phase}.actions.set_priority.priority`}
path={`phases.${phase}.actions.set_priority.priority`}
component={NumericField}
componentProps={{
fullWidth: false,
euiFieldProps: {
'data-test-subj': `${phase}-${phaseIndexPriorityProperty}`,
min: 1,
},
}}
/>
</EuiDescribedFormGroup>
);
};

View file

@ -18,8 +18,12 @@ import {
EuiDescribedFormGroup,
} from '@elastic/eui';
import { Phases, WarmPhase as WarmPhaseInterface } from '../../../../../common/types';
import { PhaseValidationErrors } from '../../../services/policies/policy_validation';
import { useFormData } from '../../../../../shared_imports';
import { Phases, WarmPhase as WarmPhaseInterface } from '../../../../../../common/types';
import { PhaseValidationErrors } from '../../../../services/policies/policy_validation';
import { useRolloverPath } from './shared';
import {
LearnMoreLink,
ActiveBadge,
@ -30,7 +34,8 @@ import {
MinAgeInput,
DescribedFormField,
Forcemerge,
} from '../components';
} from '../';
import { DataTierAllocationField } from './shared';
const i18nTexts = {
@ -61,15 +66,16 @@ interface Props {
phaseData: WarmPhaseInterface;
isShowingErrors: boolean;
errors?: PhaseValidationErrors<WarmPhaseInterface>;
hotPhaseRolloverEnabled: boolean;
}
export const WarmPhase: FunctionComponent<Props> = ({
setPhaseData,
phaseData,
errors,
isShowingErrors,
hotPhaseRolloverEnabled,
}) => {
const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({
watch: [useRolloverPath],
});
return (
<div id="warmPhaseContent" aria-live="polite" role="region" aria-relevant="additions">
<>
@ -132,7 +138,7 @@ export const WarmPhase: FunctionComponent<Props> = ({
/>
</EuiFormRow>
) : null}
{!phaseData.warmPhaseOnRollover ? (
{!phaseData.warmPhaseOnRollover || !hotPhaseRolloverEnabled ? (
<Fragment>
<EuiSpacer size="m" />
<MinAgeInput<WarmPhaseInterface>

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@ -17,36 +18,108 @@ import {
EuiSpacer,
EuiText,
EuiTitle,
EuiCallOut,
EuiLoadingSpinner,
} from '@elastic/eui';
import { Policy, PolicyFromES } from '../../../../../common/types';
import { serializePolicy } from '../../../services/policies/policy_serialization';
import { SerializedPolicy } from '../../../../../common/types';
import { useFormContext, useFormData } from '../../../../shared_imports';
interface Props {
legacyPolicy: SerializedPolicy;
close: () => void;
policy: Policy;
existingPolicy?: PolicyFromES;
policyName: string;
}
export const PolicyJsonFlyout: React.FunctionComponent<Props> = ({
close,
policy,
policyName,
existingPolicy,
close,
legacyPolicy,
}) => {
const { phases } = serializePolicy(policy, existingPolicy?.policy);
const json = JSON.stringify(
{
policy: {
phases,
},
},
null,
2
);
/**
* policy === undefined: we are checking validity
* policy === null: we have determined the policy is invalid
* policy === {@link SerializedPolicy} we have determined the policy is valid
*/
const [policy, setPolicy] = useState<undefined | null | SerializedPolicy>(undefined);
const endpoint = `PUT _ilm/policy/${policyName || '<policyName>'}`;
const request = `${endpoint}\n${json}`;
const form = useFormContext();
const [formData, getFormData] = useFormData();
useEffect(() => {
(async function checkPolicy() {
setPolicy(undefined);
if (await form.validate()) {
const p = getFormData() as SerializedPolicy;
setPolicy({
...legacyPolicy,
phases: {
...legacyPolicy.phases,
hot: p.phases.hot,
},
});
} else {
setPolicy(null);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form, legacyPolicy, formData]);
let content: React.ReactNode;
switch (policy) {
case undefined:
content = <EuiLoadingSpinner />;
break;
case null:
content = (
<EuiCallOut
iconType="alert"
color="danger"
title={i18n.translate(
'xpack.indexLifecycleMgmt.policyJsonFlyout.validationErrorCallout.title',
{ defaultMessage: 'Invalid policy' }
)}
>
{i18n.translate('xpack.indexLifecycleMgmt.policyJsonFlyout.validationErrorCallout.body', {
defaultMessage: 'To view the JSON for this policy address all validation errors.',
})}
</EuiCallOut>
);
break;
default:
const { phases } = policy;
const json = JSON.stringify(
{
policy: {
phases,
},
},
null,
2
);
const endpoint = `PUT _ilm/policy/${policyName || '<policyName>'}`;
const request = `${endpoint}\n${json}`;
content = (
<>
<EuiText>
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.policyJsonFlyout.descriptionText"
defaultMessage="This Elasticsearch request will create or update this index lifecycle policy."
/>
</p>
</EuiText>
<EuiSpacer />
<EuiCodeBlock language="json" isCopyable>
{request}
</EuiCodeBlock>
</>
);
break;
}
return (
<EuiFlyout maxWidth={480} onClose={close}>
@ -69,22 +142,7 @@ export const PolicyJsonFlyout: React.FunctionComponent<Props> = ({
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.policyJsonFlyout.descriptionText"
defaultMessage="This Elasticsearch request will create or update this index lifecycle policy."
/>
</p>
</EuiText>
<EuiSpacer />
<EuiCodeBlock language="json" isCopyable>
{request}
</EuiCodeBlock>
</EuiFlyoutBody>
<EuiFlyoutBody>{content}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButtonEmpty iconType="cross" onClick={close} flush="left">

View file

@ -3,6 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/**
* PLEASE NOTE: This component is currently duplicated. A version of this component wired up with
* the form lib lives in ./phases/shared
*/
import React, { Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui';

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const useRolloverPath = '_meta.hot.useRollover';
/**
* These strings describe the path to their respective values in the serialized
* ILM form.
*/
export const ROLLOVER_FORM_PATHS = {
maxDocs: 'phases.hot.actions.rollover.max_docs',
maxAge: 'phases.hot.actions.rollover.max_age',
maxSize: 'phases.hot.actions.rollover.max_size',
};

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { produce } from 'immer';
import { SerializedPolicy } from '../../../../common/types';
import { splitSizeAndUnits } from '../../services/policies/policy_serialization';
import { FormInternal } from './types';
export const deserializer = (policy: SerializedPolicy): FormInternal =>
produce<FormInternal>(
{
...policy,
_meta: {
hot: {
useRollover: Boolean(policy.phases.hot?.actions?.rollover),
forceMergeEnabled: Boolean(policy.phases.hot?.actions?.forcemerge),
bestCompression:
policy.phases.hot?.actions?.forcemerge?.index_codec === 'best_compression',
},
},
},
(draft) => {
if (draft.phases.hot?.actions?.rollover) {
if (draft.phases.hot.actions.rollover.max_size) {
const maxSize = splitSizeAndUnits(draft.phases.hot.actions.rollover.max_size);
draft.phases.hot.actions.rollover.max_size = maxSize.size;
draft._meta.hot.maxStorageSizeUnit = maxSize.units;
}
if (draft.phases.hot.actions.rollover.max_age) {
const maxAge = splitSizeAndUnits(draft.phases.hot.actions.rollover.max_age);
draft.phases.hot.actions.rollover.max_age = maxAge.size;
draft._meta.hot.maxAgeUnit = maxAge.units;
}
}
}
);

View file

@ -4,8 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useEffect, useState, useCallback } from 'react';
import React, { Fragment, useEffect, useState, useCallback, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@ -27,23 +29,43 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { useForm, Form } from '../../../shared_imports';
import { toasts } from '../../services/notification';
import { Phases, Policy, PolicyFromES } from '../../../../common/types';
import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types';
import { defaultPolicy } from '../../constants';
import {
validatePolicy,
ValidationErrors,
findFirstError,
} from '../../services/policies/policy_validation';
import { savePolicy } from '../../services/policies/policy_save';
import {
deserializePolicy,
getPolicyByName,
initializeNewPolicy,
legacySerializePolicy,
} from '../../services/policies/policy_serialization';
import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components';
import { ColdPhase, DeletePhase, HotPhase, WarmPhase } from './phases';
import {
ErrableFormRow,
LearnMoreLink,
PolicyJsonFlyout,
ColdPhase,
DeletePhase,
HotPhase,
WarmPhase,
} from './components';
import { schema } from './form_schema';
import { deserializer } from './deserializer';
import { createSerializer } from './serializer';
export interface Props {
policies: PolicyFromES[];
@ -57,6 +79,20 @@ export interface Props {
) => string;
history: RouteComponentProps['history'];
}
const mergeAllSerializedPolicies = (
serializedPolicy: SerializedPolicy,
legacySerializedPolicy: SerializedPolicy
): SerializedPolicy => {
return {
...legacySerializedPolicy,
phases: {
...legacySerializedPolicy.phases,
hot: serializedPolicy.phases.hot,
},
};
};
export const EditPolicy: React.FunctionComponent<Props> = ({
policies,
policyName,
@ -73,7 +109,18 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
const existingPolicy = getPolicyByName(policies, policyName);
const [policy, setPolicy] = useState<Policy>(
const serializer = useMemo(() => {
return createSerializer(existingPolicy?.policy);
}, [existingPolicy?.policy]);
const { form } = useForm({
schema,
defaultValue: existingPolicy?.policy ?? defaultPolicy,
deserializer,
serializer,
});
const [policy, setPolicy] = useState<LegacyPolicy>(() =>
existingPolicy ? deserializePolicy(existingPolicy) : initializeNewPolicy(policyName)
);
@ -85,9 +132,26 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
history.push('/policies');
};
const setWarmPhaseOnRollover = useCallback(
(value: boolean) => {
setPolicy((p) => ({
...p,
phases: {
...p.phases,
warm: {
...p.phases.warm,
warmPhaseOnRollover: value,
},
},
}));
},
[setPolicy]
);
const submit = async () => {
setIsShowingErrors(true);
const [isValid, validationErrors] = validatePolicy(
const { data: formLibPolicy, isValid: newIsValid } = await form.submit();
const [legacyIsValid, validationErrors] = validatePolicy(
saveAsNew,
policy,
policies,
@ -95,20 +159,30 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
);
setErrors(validationErrors);
const isValid = legacyIsValid && newIsValid;
if (!isValid) {
toasts.addDanger(
i18n.translate('xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage', {
defaultMessage: 'Please fix the errors on this page.',
})
);
const firstError = findFirstError(validationErrors);
const errorRowId = `${firstError ? firstError.replace('.', '-') : ''}-row`;
const element = document.getElementById(errorRowId);
if (element) {
element.scrollIntoView({ block: 'center', inline: 'nearest' });
// This functionality will not be required for once form lib is fully adopted for this form
// because errors are reported as fields are edited.
if (!legacyIsValid) {
const firstError = findFirstError(validationErrors);
const errorRowId = `${firstError ? firstError.replace('.', '-') : ''}-row`;
const element = document.getElementById(errorRowId);
if (element) {
element.scrollIntoView({ block: 'center', inline: 'nearest' });
}
}
} else {
const success = await savePolicy(policy, isNewPolicy || saveAsNew, existingPolicy);
const readSerializedPolicy = () => {
const legacySerializedPolicy = legacySerializePolicy(policy, existingPolicy?.policy);
return mergeAllSerializedPolicies(formLibPolicy, legacySerializedPolicy);
};
const success = await savePolicy(readSerializedPolicy, isNewPolicy || saveAsNew);
if (success) {
backToPolicyList();
}
@ -120,7 +194,7 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
};
const setPhaseData = useCallback(
(phase: keyof Phases, key: string, value: any) => {
(phase: keyof LegacyPolicy['phases'], key: string, value: any) => {
setPolicy((nextPolicy) => ({
...nextPolicy,
phases: {
@ -132,10 +206,6 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
[setPolicy]
);
const setHotPhaseData = useCallback(
(key: string, value: any) => setPhaseData('hot', key, value),
[setPhaseData]
);
const setWarmPhaseData = useCallback(
(key: string, value: any) => setPhaseData('warm', key, value),
[setPhaseData]
@ -149,23 +219,6 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
[setPhaseData]
);
const setWarmPhaseOnRollover = (value: boolean) => {
setPolicy({
...policy,
phases: {
...policy.phases,
hot: {
...policy.phases.hot,
rolloverEnabled: value,
},
warm: {
...policy.phases.warm,
warmPhaseOnRollover: value,
},
},
});
};
return (
<EuiPage>
<EuiPageBody>
@ -188,215 +241,210 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
</EuiTitle>
<div className="euiAnimateContentLoad">
<EuiSpacer size="xs" />
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText"
defaultMessage="Use an index policy to automate the four phases of the index lifecycle,
<Form form={form}>
<EuiSpacer size="xs" />
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText"
defaultMessage="Use an index policy to automate the four phases of the index lifecycle,
from actively writing to the index to deleting it."
/>{' '}
<LearnMoreLink
docPath="index-lifecycle-management.html"
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexLifecycleManagementLinkText"
defaultMessage="Learn about the index lifecycle."
/>
}
/>
</p>
</EuiText>
<EuiSpacer />
{isNewPolicy ? null : (
<Fragment>
<EuiText>
<p>
<strong>
/>{' '}
<LearnMoreLink
docPath="index-lifecycle-management.html"
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.editingExistingPolicyMessage"
defaultMessage="You are editing an existing policy"
id="xpack.indexLifecycleMgmt.editPolicy.learnAboutIndexLifecycleManagementLinkText"
defaultMessage="Learn about the index lifecycle."
/>
</strong>
.{' '}
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.editingExistingPolicyExplanationMessage"
defaultMessage="Any changes you make will affect the indices that are
attached to this policy. Alternatively, you can save these changes in
a new policy."
/>
</p>
</EuiText>
<EuiSpacer />
<EuiFormRow>
<EuiSwitch
data-test-subj="saveAsNewSwitch"
style={{ maxWidth: '100%' }}
checked={saveAsNew}
onChange={(e) => {
setSaveAsNew(e.target.checked);
}}
label={
<span>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage"
defaultMessage="Save as new policy"
/>
</span>
}
/>
</EuiFormRow>
</Fragment>
)}
</p>
</EuiText>
{saveAsNew || isNewPolicy ? (
<EuiDescribedFormGroup
title={
<div>
<span className="eui-displayInlineBlock eui-alignMiddle">
<EuiSpacer />
{isNewPolicy ? null : (
<Fragment>
<EuiText>
<p>
<strong>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.editingExistingPolicyMessage"
defaultMessage="You are editing an existing policy"
/>
</strong>
.{' '}
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nameLabel"
defaultMessage="Name"
id="xpack.indexLifecycleMgmt.editPolicy.editingExistingPolicyExplanationMessage"
defaultMessage="Any changes you make will affect the indices that are
attached to this policy. Alternatively, you can save these changes in
a new policy."
/>
</span>
</div>
}
titleSize="s"
fullWidth
>
<ErrableFormRow
id={'policyName'}
label={i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameLabel', {
defaultMessage: 'Policy name',
})}
isShowingErrors={isShowingErrors}
errors={errors?.policyName}
helpText={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage"
defaultMessage="A policy name cannot start with an underscore and cannot contain a question mark or a space."
</p>
</EuiText>
<EuiSpacer />
<EuiFormRow>
<EuiSwitch
data-test-subj="saveAsNewSwitch"
style={{ maxWidth: '100%' }}
checked={saveAsNew}
onChange={(e) => {
setSaveAsNew(e.target.checked);
}}
label={
<span>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage"
defaultMessage="Save as new policy"
/>
</span>
}
/>
</EuiFormRow>
</Fragment>
)}
{saveAsNew || isNewPolicy ? (
<EuiDescribedFormGroup
title={
<div>
<span className="eui-displayInlineBlock eui-alignMiddle">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nameLabel"
defaultMessage="Name"
/>
</span>
</div>
}
titleSize="s"
fullWidth
>
<EuiFieldText
data-test-subj="policyNameField"
value={policy.name}
onChange={(e) => {
setPolicy({ ...policy, name: e.target.value });
}}
/>
</ErrableFormRow>
</EuiDescribedFormGroup>
) : null}
<EuiSpacer />
<HotPhase
errors={errors?.hot}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.hot).length > 0}
setPhaseData={setHotPhaseData}
phaseData={policy.phases.hot}
setWarmPhaseOnRollover={setWarmPhaseOnRollover}
/>
<EuiHorizontalRule />
<WarmPhase
errors={errors?.warm}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.warm).length > 0}
setPhaseData={setWarmPhaseData}
phaseData={policy.phases.warm}
hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled}
/>
<EuiHorizontalRule />
<ColdPhase
errors={errors?.cold}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.cold).length > 0}
setPhaseData={setColdPhaseData}
phaseData={policy.phases.cold}
hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled}
/>
<EuiHorizontalRule />
<DeletePhase
errors={errors?.delete}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.delete).length > 0}
getUrlForApp={getUrlForApp}
setPhaseData={setDeletePhaseData}
phaseData={policy.phases.delete}
hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled}
/>
<EuiHorizontalRule />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="savePolicyButton"
fill
iconType="check"
iconSide="left"
onClick={submit}
color="secondary"
>
{saveAsNew ? (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.saveAsNewButton"
defaultMessage="Save as new policy"
/>
) : (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.saveButton"
defaultMessage="Save policy"
/>
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelTestPolicy" onClick={backToPolicyList}>
<ErrableFormRow
id={'policyName'}
label={i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameLabel', {
defaultMessage: 'Policy name',
})}
isShowingErrors={isShowingErrors}
errors={errors?.policyName}
helpText={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.cancelButton"
defaultMessage="Cancel"
id="xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage"
defaultMessage="A policy name cannot start with an underscore and cannot contain a question mark or a space."
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={togglePolicyJsonFlyout} data-test-subj="requestButton">
{isShowingPolicyJsonFlyout ? (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hidePolicyJsonButto"
defaultMessage="Hide request"
}
>
<EuiFieldText
data-test-subj="policyNameField"
value={policy.name}
onChange={(e) => {
setPolicy({ ...policy, name: e.target.value });
}}
/>
) : (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButto"
defaultMessage="Show request"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</ErrableFormRow>
</EuiDescribedFormGroup>
) : null}
{isShowingPolicyJsonFlyout ? (
<PolicyJsonFlyout
policyName={policy.name || ''}
existingPolicy={existingPolicy}
policy={policy}
close={() => setIsShowingPolicyJsonFlyout(false)}
<EuiSpacer />
<HotPhase setWarmPhaseOnRollover={setWarmPhaseOnRollover} />
<EuiHorizontalRule />
<WarmPhase
errors={errors?.warm}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.warm).length > 0}
setPhaseData={setWarmPhaseData}
phaseData={policy.phases.warm}
/>
) : null}
<EuiHorizontalRule />
<ColdPhase
errors={errors?.cold}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.cold).length > 0}
setPhaseData={setColdPhaseData}
phaseData={policy.phases.cold}
/>
<EuiHorizontalRule />
<DeletePhase
errors={errors?.delete}
isShowingErrors={
isShowingErrors && !!errors && Object.keys(errors.delete).length > 0
}
getUrlForApp={getUrlForApp}
setPhaseData={setDeletePhaseData}
phaseData={policy.phases.delete}
/>
<EuiHorizontalRule />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="savePolicyButton"
fill
iconType="check"
iconSide="left"
disabled={form.isValid === false || form.isSubmitting}
onClick={submit}
color="secondary"
>
{saveAsNew ? (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.saveAsNewButton"
defaultMessage="Save as new policy"
/>
) : (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.saveButton"
defaultMessage="Save policy"
/>
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelTestPolicy" onClick={backToPolicyList}>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.cancelButton"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={togglePolicyJsonFlyout} data-test-subj="requestButton">
{isShowingPolicyJsonFlyout ? (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hidePolicyJsonButto"
defaultMessage="Hide request"
/>
) : (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButto"
defaultMessage="Show request"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
{isShowingPolicyJsonFlyout ? (
<PolicyJsonFlyout
policyName={policy.name || ''}
legacyPolicy={legacySerializePolicy(policy, existingPolicy?.policy)}
close={() => setIsShowingPolicyJsonFlyout(false)}
/>
) : null}
</Form>
</div>
</EuiPageContent>
</EuiPageBody>

View file

@ -0,0 +1,129 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { FormSchema, fieldValidators } from '../../../shared_imports';
import { defaultSetPriority } from '../../constants';
import { FormInternal } from './types';
import { ifExistsNumberGreaterThanZero, rolloverThresholdsValidator } from './form_validations';
import { i18nTexts } from './i18n_texts';
const { emptyField } = fieldValidators;
export const schema: FormSchema<FormInternal> = {
_meta: {
hot: {
useRollover: {
defaultValue: true,
label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', {
defaultMessage: 'Enable rollover',
}),
},
maxStorageSizeUnit: {
defaultValue: 'gb',
},
maxAgeUnit: {
defaultValue: 'd',
},
forceMergeEnabled: {
label: i18nTexts.editPolicy.forceMergeEnabledFieldLabel,
},
bestCompression: {
label: i18n.translate('xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel', {
defaultMessage: 'Compress stored fields',
}),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.forceMerge.bestCompressionText',
{
defaultMessage:
'Use higher compression for stored fields at the cost of slower performance.',
}
),
},
},
},
phases: {
hot: {
actions: {
rollover: {
max_age: {
label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumAgeLabel', {
defaultMessage: 'Maximum age',
}),
validations: [
{
validator: rolloverThresholdsValidator,
},
{
validator: ifExistsNumberGreaterThanZero,
},
],
},
max_docs: {
label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumDocumentsLabel', {
defaultMessage: 'Maximum documents',
}),
validations: [
{
validator: rolloverThresholdsValidator,
},
{
validator: ifExistsNumberGreaterThanZero,
},
],
serializer: (v: string): any => (v ? parseInt(v, 10) : undefined),
},
max_size: {
label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeLabel', {
defaultMessage: 'Maximum index size',
}),
validations: [
{
validator: rolloverThresholdsValidator,
},
{
validator: ifExistsNumberGreaterThanZero,
},
],
},
},
forcemerge: {
max_num_segments: {
label: i18n.translate('xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel', {
defaultMessage: 'Number of segments',
}),
validations: [
{
validator: emptyField(
i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError',
{ defaultMessage: 'A value for number of segments is required.' }
)
),
},
{
validator: ifExistsNumberGreaterThanZero,
},
],
serializer: (v: string): any => (v ? parseInt(v, 10) : undefined),
},
},
set_priority: {
priority: {
defaultValue: defaultSetPriority as any,
label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.indexPriorityLabel', {
defaultMessage: 'Index priority (optional)',
}),
validations: [{ validator: ifExistsNumberGreaterThanZero }],
serializer: (v: string): any => (v ? parseInt(v, 10) : undefined),
},
},
},
},
},
};

View file

@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { fieldValidators, ValidationFunc } from '../../../shared_imports';
import { i18nTexts } from './components/phases/hot_phase/i18n_texts';
import { ROLLOVER_FORM_PATHS } from './constants';
const { numberGreaterThanField } = fieldValidators;
export const positiveNumberRequiredMessage = i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.numberAboveZeroRequiredError',
{
defaultMessage: 'Only numbers above 0 are allowed.',
}
);
export const ifExistsNumberGreaterThanZero: ValidationFunc<any, any, any> = (arg) => {
if (arg.value) {
return numberGreaterThanField({
than: 0,
message: positiveNumberRequiredMessage,
})({
...arg,
value: parseInt(arg.value, 10),
});
}
};
/**
* A special validation type used to keep track of validation errors for
* the rollover threshold values not being set (e.g., age and doc count)
*/
export const ROLLOVER_EMPTY_VALIDATION = 'EMPTY';
/**
* An ILM policy requires that for rollover a value must be set for one of the threshold values.
*
* This validator checks that and updates form values by setting errors states imperatively to
* indicate this error state.
*/
export const rolloverThresholdsValidator: ValidationFunc = ({ form }) => {
const fields = form.getFields();
if (
!(
fields[ROLLOVER_FORM_PATHS.maxAge].value ||
fields[ROLLOVER_FORM_PATHS.maxDocs].value ||
fields[ROLLOVER_FORM_PATHS.maxSize].value
)
) {
fields[ROLLOVER_FORM_PATHS.maxAge].setErrors([
{ validationType: ROLLOVER_EMPTY_VALIDATION, message: i18nTexts.maximumAgeRequiredMessage },
]);
fields[ROLLOVER_FORM_PATHS.maxDocs].setErrors([
{
validationType: ROLLOVER_EMPTY_VALIDATION,
message: i18nTexts.maximumDocumentsRequiredMessage,
},
]);
fields[ROLLOVER_FORM_PATHS.maxSize].setErrors([
{ validationType: ROLLOVER_EMPTY_VALIDATION, message: i18nTexts.maximumSizeRequiredMessage },
]);
} else {
fields[ROLLOVER_FORM_PATHS.maxAge].clearErrors(ROLLOVER_EMPTY_VALIDATION);
fields[ROLLOVER_FORM_PATHS.maxDocs].clearErrors(ROLLOVER_EMPTY_VALIDATION);
fields[ROLLOVER_FORM_PATHS.maxSize].clearErrors(ROLLOVER_EMPTY_VALIDATION);
}
};

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const i18nTexts = {
editPolicy: {
forceMergeEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.forcemerge.enableLabel', {
defaultMessage: 'Force merge data',
}),
},
};

View file

@ -1,153 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { PureComponent, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
import { DeletePhase as DeletePhaseInterface, Phases } from '../../../../../common/types';
import { PhaseValidationErrors } from '../../../services/policies/policy_validation';
import {
ActiveBadge,
LearnMoreLink,
OptionalLabel,
PhaseErrorMessage,
MinAgeInput,
SnapshotPolicies,
} from '../components';
const deleteProperty: keyof Phases = 'delete';
const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName;
interface Props {
setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void;
phaseData: DeletePhaseInterface;
isShowingErrors: boolean;
errors?: PhaseValidationErrors<DeletePhaseInterface>;
hotPhaseRolloverEnabled: boolean;
getUrlForApp: (
appId: string,
options?: {
path?: string;
absolute?: boolean;
}
) => string;
}
export class DeletePhase extends PureComponent<Props> {
render() {
const {
setPhaseData,
phaseData,
errors,
isShowingErrors,
hotPhaseRolloverEnabled,
getUrlForApp,
} = this.props;
return (
<div id="deletePhaseContent" aria-live="polite" role="region">
<EuiDescribedFormGroup
title={
<div>
<h2 className="eui-displayInlineBlock eui-alignMiddle">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseLabel"
defaultMessage="Delete phase"
/>
</h2>{' '}
{phaseData.phaseEnabled && !isShowingErrors ? <ActiveBadge /> : null}
<PhaseErrorMessage isShowingErrors={isShowingErrors} />
</div>
}
titleSize="s"
description={
<Fragment>
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescriptionText"
defaultMessage="You no longer need your index. You can define when it is safe to delete it."
/>
</p>
<EuiSwitch
data-test-subj="enablePhaseSwitch-delete"
label={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.activateWarmPhaseSwitchLabel"
defaultMessage="Activate delete phase"
/>
}
id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`}
checked={phaseData.phaseEnabled}
onChange={(e) => {
setPhaseData(phaseProperty('phaseEnabled'), e.target.checked);
}}
aria-controls="deletePhaseContent"
/>
</Fragment>
}
fullWidth
>
{phaseData.phaseEnabled ? (
<MinAgeInput<DeletePhaseInterface>
errors={errors}
phaseData={phaseData}
phase={deleteProperty}
isShowingErrors={isShowingErrors}
setPhaseData={setPhaseData}
rolloverEnabled={hotPhaseRolloverEnabled}
/>
) : (
<div />
)}
</EuiDescribedFormGroup>
{phaseData.phaseEnabled ? (
<EuiDescribedFormGroup
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.waitForSnapshotTitle"
defaultMessage="Wait for snapshot policy"
/>
</h3>
}
description={
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.waitForSnapshotDescription"
defaultMessage="Specify a snapshot policy to be executed before the deletion of the index. This ensures that a snapshot of the deleted index is available."
/>{' '}
<LearnMoreLink docPath="ilm-wait-for-snapshot.html" />
</EuiTextColor>
}
titleSize="xs"
fullWidth
>
<EuiFormRow
id="deletePhaseWaitForSnapshot"
label={
<Fragment>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.waitForSnapshotLabel"
defaultMessage="Snapshot policy name"
/>
<OptionalLabel />
</Fragment>
}
>
<SnapshotPolicies
value={phaseData.waitForSnapshotPolicy}
onChange={(value) => setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)}
getUrlForApp={getUrlForApp}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
) : null}
</div>
);
}
}

View file

@ -1,336 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, PureComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiFieldNumber,
EuiSelect,
EuiSwitch,
EuiFormRow,
EuiDescribedFormGroup,
} from '@elastic/eui';
import { HotPhase as HotPhaseInterface, Phases } from '../../../../../common/types';
import { PhaseValidationErrors } from '../../../services/policies/policy_validation';
import {
LearnMoreLink,
ActiveBadge,
PhaseErrorMessage,
ErrableFormRow,
SetPriorityInput,
Forcemerge,
} from '../components';
const maxSizeStoredUnits = [
{
value: 'gb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.gigabytesLabel', {
defaultMessage: 'gigabytes',
}),
},
{
value: 'mb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.megabytesLabel', {
defaultMessage: 'megabytes',
}),
},
{
value: 'b',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.bytesLabel', {
defaultMessage: 'bytes',
}),
},
{
value: 'kb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.kilobytesLabel', {
defaultMessage: 'kilobytes',
}),
},
{
value: 'tb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.terabytesLabel', {
defaultMessage: 'terabytes',
}),
},
{
value: 'pb',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.petabytesLabel', {
defaultMessage: 'petabytes',
}),
},
];
const maxAgeUnits = [
{
value: 'd',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.daysLabel', {
defaultMessage: 'days',
}),
},
{
value: 'h',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.hoursLabel', {
defaultMessage: 'hours',
}),
},
{
value: 'm',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.minutesLabel', {
defaultMessage: 'minutes',
}),
},
{
value: 's',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.secondsLabel', {
defaultMessage: 'seconds',
}),
},
{
value: 'ms',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.millisecondsLabel', {
defaultMessage: 'milliseconds',
}),
},
{
value: 'micros',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.microsecondsLabel', {
defaultMessage: 'microseconds',
}),
},
{
value: 'nanos',
text: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.nanosecondsLabel', {
defaultMessage: 'nanoseconds',
}),
},
];
const hotProperty: keyof Phases = 'hot';
const phaseProperty = (propertyName: keyof HotPhaseInterface) => propertyName;
interface Props {
errors?: PhaseValidationErrors<HotPhaseInterface>;
isShowingErrors: boolean;
phaseData: HotPhaseInterface;
setPhaseData: (key: keyof HotPhaseInterface & string, value: string | boolean) => void;
setWarmPhaseOnRollover: (value: boolean) => void;
}
export class HotPhase extends PureComponent<Props> {
render() {
const { setPhaseData, phaseData, isShowingErrors, errors, setWarmPhaseOnRollover } = this.props;
return (
<Fragment>
<EuiDescribedFormGroup
title={
<div>
<h2 className="eui-displayInlineBlock eui-alignMiddle">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseLabel"
defaultMessage="Hot phase"
/>
</h2>{' '}
{isShowingErrors ? null : <ActiveBadge />}
<PhaseErrorMessage isShowingErrors={isShowingErrors} />
</div>
}
titleSize="s"
description={
<Fragment>
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescriptionMessage"
defaultMessage="This phase is required. You are actively querying and
writing to your index. For faster updates, you can roll over the index when it gets too big or too old."
/>
</p>
</Fragment>
}
fullWidth
>
<EuiFormRow
id="rolloverFormRow"
hasEmptyLabelSpace
helpText={
<Fragment>
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hotPhase.rolloverDescriptionMessage"
defaultMessage="The new index created by rollover is added
to the index alias and designated as the write index."
/>
</p>
<LearnMoreLink
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.hotPhase.learnAboutRolloverLinkText"
defaultMessage="Learn about rollover"
/>
}
docPath="indices-rollover-index.html"
/>
<EuiSpacer size="m" />
</Fragment>
}
>
<EuiSwitch
data-test-subj="rolloverSwitch"
checked={phaseData.rolloverEnabled}
onChange={async (e) => {
setWarmPhaseOnRollover(e.target.checked);
}}
label={i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', {
defaultMessage: 'Enable rollover',
})}
/>
</EuiFormRow>
{phaseData.rolloverEnabled ? (
<Fragment>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem style={{ maxWidth: 188 }}>
<ErrableFormRow
id={`${hotProperty}-${phaseProperty('selectedMaxSizeStored')}`}
label={i18n.translate(
'xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeLabel',
{
defaultMessage: 'Maximum index size',
}
)}
isShowingErrors={isShowingErrors}
errors={errors?.selectedMaxSizeStored}
>
<EuiFieldNumber
id={`${hotProperty}-${phaseProperty('selectedMaxSizeStored')}`}
value={phaseData.selectedMaxSizeStored}
onChange={(e) => {
setPhaseData(phaseProperty('selectedMaxSizeStored'), e.target.value);
}}
min={1}
/>
</ErrableFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ maxWidth: 188 }}>
<ErrableFormRow
id={`${hotProperty}-${phaseProperty('selectedMaxSizeStoredUnits')}`}
hasEmptyLabelSpace
isShowingErrors={isShowingErrors}
errors={errors?.selectedMaxSizeStoredUnits}
>
<EuiSelect
aria-label={i18n.translate(
'xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeUnitsAriaLabel',
{
defaultMessage: 'Maximum index size units',
}
)}
value={phaseData.selectedMaxSizeStoredUnits}
onChange={(e) => {
setPhaseData(phaseProperty('selectedMaxSizeStoredUnits'), e.target.value);
}}
options={maxSizeStoredUnits}
/>
</ErrableFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem style={{ maxWidth: 188 }}>
<ErrableFormRow
id={`${hotProperty}-${phaseProperty('selectedMaxDocuments')}`}
label={i18n.translate(
'xpack.indexLifecycleMgmt.hotPhase.maximumDocumentsLabel',
{
defaultMessage: 'Maximum documents',
}
)}
isShowingErrors={isShowingErrors}
errors={errors?.selectedMaxDocuments}
>
<EuiFieldNumber
id={`${hotProperty}-${phaseProperty('selectedMaxDocuments')}`}
value={phaseData.selectedMaxDocuments}
onChange={(e) => {
setPhaseData(phaseProperty('selectedMaxDocuments'), e.target.value);
}}
min={1}
/>
</ErrableFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem style={{ maxWidth: 188 }}>
<ErrableFormRow
id={`${hotProperty}-${phaseProperty('selectedMaxAge')}`}
label={i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumAgeLabel', {
defaultMessage: 'Maximum age',
})}
isShowingErrors={isShowingErrors}
errors={errors?.selectedMaxAge}
>
<EuiFieldNumber
id={`${hotProperty}-${phaseProperty('selectedMaxAge')}`}
value={phaseData.selectedMaxAge}
onChange={(e) => {
setPhaseData(phaseProperty('selectedMaxAge'), e.target.value);
}}
min={1}
/>
</ErrableFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ maxWidth: 188 }}>
<ErrableFormRow
id={`${hotProperty}-${phaseProperty('selectedMaxAgeUnits')}`}
hasEmptyLabelSpace
isShowingErrors={isShowingErrors}
errors={errors?.selectedMaxAgeUnits}
>
<EuiSelect
aria-label={i18n.translate(
'xpack.indexLifecycleMgmt.hotPhase.maximumAgeUnitsAriaLabel',
{
defaultMessage: 'Maximum age units',
}
)}
value={phaseData.selectedMaxAgeUnits}
onChange={(e) => {
setPhaseData(phaseProperty('selectedMaxAgeUnits'), e.target.value);
}}
options={maxAgeUnits}
/>
</ErrableFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
) : null}
</EuiDescribedFormGroup>
{phaseData.rolloverEnabled ? (
<Forcemerge
phase={'hot'}
phaseData={phaseData}
setPhaseData={setPhaseData}
isShowingErrors={isShowingErrors}
errors={errors}
/>
) : null}
<SetPriorityInput<HotPhaseInterface>
errors={errors}
phaseData={phaseData}
phase={hotProperty}
isShowingErrors={isShowingErrors}
setPhaseData={setPhaseData}
/>
</Fragment>
);
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SerializedPolicy } from '../../../../common/types';
import { FormInternal } from './types';
export const createSerializer = (originalPolicy?: SerializedPolicy) => (
data: FormInternal
): SerializedPolicy => {
const { _meta, ...rest } = data;
if (!rest.phases || !rest.phases.hot) {
rest.phases = { hot: { actions: {} } };
}
if (rest.phases.hot) {
rest.phases.hot.min_age = originalPolicy?.phases.hot?.min_age ?? '0ms';
}
if (rest.phases.hot?.actions) {
if (rest.phases.hot.actions?.rollover && _meta.hot.useRollover) {
if (rest.phases.hot.actions.rollover.max_age) {
rest.phases.hot.actions.rollover.max_age = `${rest.phases.hot.actions.rollover.max_age}${_meta.hot.maxAgeUnit}`;
}
if (rest.phases.hot.actions.rollover.max_size) {
rest.phases.hot.actions.rollover.max_size = `${rest.phases.hot.actions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`;
}
if (_meta.hot.bestCompression && rest.phases.hot.actions?.forcemerge) {
rest.phases.hot.actions.forcemerge.index_codec = 'best_compression';
}
} else {
delete rest.phases.hot.actions?.rollover;
}
}
return rest;
};

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SerializedPolicy } from '../../../../common/types';
/**
* Describes the shape of data after deserialization.
*/
export interface FormInternal extends SerializedPolicy {
/**
* This is a special internal-only field that is used to display or hide
* certain form fields which affects what is ultimately serialized.
*/
_meta: {
hot: {
useRollover: boolean;
forceMergeEnabled: boolean;
bestCompression: boolean;
maxStorageSizeUnit?: string;
maxAgeUnit?: string;
};
};
}

View file

@ -1,190 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { HotPhase, SerializedHotPhase } from '../../../../common/types';
import { serializedPhaseInitialization } from '../../constants';
import { isNumber, splitSizeAndUnits } from './policy_serialization';
import {
maximumAgeRequiredMessage,
maximumDocumentsRequiredMessage,
maximumSizeRequiredMessage,
numberRequiredMessage,
PhaseValidationErrors,
positiveNumberRequiredMessage,
positiveNumbersAboveZeroErrorMessage,
} from './policy_validation';
const hotPhaseInitialization: HotPhase = {
phaseEnabled: false,
rolloverEnabled: false,
selectedMaxAge: '',
selectedMaxAgeUnits: 'd',
selectedMaxSizeStored: '',
selectedMaxSizeStoredUnits: 'gb',
forceMergeEnabled: false,
selectedForceMergeSegments: '',
bestCompressionEnabled: false,
phaseIndexPriority: '',
selectedMaxDocuments: '',
};
export const hotPhaseFromES = (phaseSerialized?: SerializedHotPhase): HotPhase => {
const phase: HotPhase = { ...hotPhaseInitialization };
if (phaseSerialized === undefined || phaseSerialized === null) {
return phase;
}
phase.phaseEnabled = true;
if (phaseSerialized.actions) {
const actions = phaseSerialized.actions;
if (actions.rollover) {
const rollover = actions.rollover;
phase.rolloverEnabled = true;
if (rollover.max_age) {
const { size: maxAge, units: maxAgeUnits } = splitSizeAndUnits(rollover.max_age);
phase.selectedMaxAge = maxAge;
phase.selectedMaxAgeUnits = maxAgeUnits;
}
if (rollover.max_size) {
const { size: maxSize, units: maxSizeUnits } = splitSizeAndUnits(rollover.max_size);
phase.selectedMaxSizeStored = maxSize;
phase.selectedMaxSizeStoredUnits = maxSizeUnits;
}
if (rollover.max_docs) {
phase.selectedMaxDocuments = rollover.max_docs.toString();
}
}
if (actions.forcemerge) {
const forcemerge = actions.forcemerge;
phase.forceMergeEnabled = true;
phase.selectedForceMergeSegments = forcemerge.max_num_segments.toString();
// only accepted value for index_codec
phase.bestCompressionEnabled = forcemerge.index_codec === 'best_compression';
}
if (actions.set_priority) {
phase.phaseIndexPriority = actions.set_priority.priority
? actions.set_priority.priority.toString()
: '';
}
}
return phase;
};
export const hotPhaseToES = (
phase: HotPhase,
originalPhase?: SerializedHotPhase
): SerializedHotPhase => {
if (!originalPhase) {
originalPhase = { ...serializedPhaseInitialization };
}
const esPhase = { ...originalPhase };
esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {};
if (phase.rolloverEnabled) {
if (!esPhase.actions.rollover) {
esPhase.actions.rollover = {};
}
if (isNumber(phase.selectedMaxAge)) {
esPhase.actions.rollover.max_age = `${phase.selectedMaxAge}${phase.selectedMaxAgeUnits}`;
}
if (isNumber(phase.selectedMaxSizeStored)) {
esPhase.actions.rollover.max_size = `${phase.selectedMaxSizeStored}${phase.selectedMaxSizeStoredUnits}`;
}
if (isNumber(phase.selectedMaxDocuments)) {
esPhase.actions.rollover.max_docs = parseInt(phase.selectedMaxDocuments, 10);
}
if (phase.forceMergeEnabled && isNumber(phase.selectedForceMergeSegments)) {
esPhase.actions.forcemerge = {
max_num_segments: parseInt(phase.selectedForceMergeSegments, 10),
};
if (phase.bestCompressionEnabled) {
// only accepted value for index_codec
esPhase.actions.forcemerge.index_codec = 'best_compression';
}
} else {
delete esPhase.actions.forcemerge;
}
} else {
delete esPhase.actions.rollover;
// forcemerge is only allowed if rollover is enabled
if (esPhase.actions.forcemerge) {
delete esPhase.actions.forcemerge;
}
}
if (isNumber(phase.phaseIndexPriority)) {
esPhase.actions.set_priority = {
priority: parseInt(phase.phaseIndexPriority, 10),
};
} else {
delete esPhase.actions.set_priority;
}
return esPhase;
};
export const validateHotPhase = (phase: HotPhase): PhaseValidationErrors<HotPhase> => {
if (!phase.phaseEnabled) {
return {};
}
const phaseErrors = {} as PhaseValidationErrors<HotPhase>;
// index priority is optional, but if it's set, it needs to be a positive number
if (phase.phaseIndexPriority) {
if (!isNumber(phase.phaseIndexPriority)) {
phaseErrors.phaseIndexPriority = [numberRequiredMessage];
} else if (parseInt(phase.phaseIndexPriority, 10) < 0) {
phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage];
}
}
// if rollover is enabled
if (phase.rolloverEnabled) {
// either max_age, max_size or max_documents need to be set
if (
!isNumber(phase.selectedMaxAge) &&
!isNumber(phase.selectedMaxSizeStored) &&
!isNumber(phase.selectedMaxDocuments)
) {
phaseErrors.selectedMaxAge = [maximumAgeRequiredMessage];
phaseErrors.selectedMaxSizeStored = [maximumSizeRequiredMessage];
phaseErrors.selectedMaxDocuments = [maximumDocumentsRequiredMessage];
}
// max age, max size and max docs need to be above zero if set
if (isNumber(phase.selectedMaxAge) && parseInt(phase.selectedMaxAge, 10) < 1) {
phaseErrors.selectedMaxAge = [positiveNumbersAboveZeroErrorMessage];
}
if (isNumber(phase.selectedMaxSizeStored) && parseInt(phase.selectedMaxSizeStored, 10) < 1) {
phaseErrors.selectedMaxSizeStored = [positiveNumbersAboveZeroErrorMessage];
}
if (isNumber(phase.selectedMaxDocuments) && parseInt(phase.selectedMaxDocuments, 10) < 1) {
phaseErrors.selectedMaxDocuments = [positiveNumbersAboveZeroErrorMessage];
}
// if forcemerge is enabled, force merge segments needs to be a number above zero
if (phase.forceMergeEnabled) {
if (!isNumber(phase.selectedForceMergeSegments)) {
phaseErrors.selectedForceMergeSegments = [numberRequiredMessage];
} else if (parseInt(phase.selectedForceMergeSegments, 10) < 1) {
phaseErrors.selectedForceMergeSegments = [positiveNumbersAboveZeroErrorMessage];
}
}
}
return {
...phaseErrors,
};
};

View file

@ -7,26 +7,25 @@
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
import { Policy, PolicyFromES } from '../../../../common/types';
import { SerializedPolicy } from '../../../../common/types';
import { savePolicy as savePolicyApi } from '../api';
import { showApiError } from '../api_errors';
import { getUiMetricsForPhases, trackUiMetric } from '../ui_metric';
import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants';
import { toasts } from '../notification';
import { serializePolicy } from './policy_serialization';
export const savePolicy = async (
policy: Policy,
isNew: boolean,
originalEsPolicy?: PolicyFromES
readSerializedPolicy: () => SerializedPolicy,
isNew: boolean
): Promise<boolean> => {
const serializedPolicy = serializePolicy(policy, originalEsPolicy?.policy);
const serializedPolicy = readSerializedPolicy();
try {
await savePolicyApi(serializedPolicy);
} catch (err) {
const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage', {
defaultMessage: 'Error saving lifecycle policy {lifecycleName}',
values: { lifecycleName: policy.name },
values: { lifecycleName: serializedPolicy.name },
});
showApiError(err, title);
return false;
@ -46,7 +45,7 @@ export const savePolicy = async (
: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.updatedMessage', {
defaultMessage: 'Updated',
}),
lifecycleName: policy.name,
lifecycleName: serializedPolicy.name,
},
});
toasts.addSuccess(message);

View file

@ -6,24 +6,18 @@
// Prefer importing entire lodash library, e.g. import { get } from "lodash"
// eslint-disable-next-line no-restricted-imports
import cloneDeep from 'lodash/cloneDeep';
import { deserializePolicy, serializePolicy } from './policy_serialization';
import {
defaultNewColdPhase,
defaultNewDeletePhase,
defaultNewHotPhase,
defaultNewWarmPhase,
} from '../../constants';
import { deserializePolicy, legacySerializePolicy } from './policy_serialization';
import { defaultNewColdPhase, defaultNewDeletePhase, defaultNewWarmPhase } from '../../constants';
import { DataTierAllocationType } from '../../../../common/types';
import { coldPhaseInitialization } from './cold_phase';
describe('Policy serialization', () => {
test('serialize a policy using "default" data allocation', () => {
expect(
serializePolicy(
legacySerializePolicy(
{
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'default',
@ -56,17 +50,6 @@ describe('Policy serialization', () => {
).toEqual({
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
set_priority: {
@ -88,11 +71,10 @@ describe('Policy serialization', () => {
test('serialize a policy using "custom" data allocation', () => {
expect(
serializePolicy(
legacySerializePolicy(
{
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'custom',
@ -136,17 +118,6 @@ describe('Policy serialization', () => {
).toEqual({
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
allocate: {
@ -182,11 +153,10 @@ describe('Policy serialization', () => {
test('serialize a policy using "custom" data allocation with no node attributes', () => {
expect(
serializePolicy(
legacySerializePolicy(
{
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'custom',
@ -219,17 +189,6 @@ describe('Policy serialization', () => {
// There should be no allocation action in any phases...
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
allocate: { include: {}, exclude: {}, require: { something: 'here' } },
@ -253,11 +212,10 @@ describe('Policy serialization', () => {
test('serialize a policy using "none" data allocation with no node attributes', () => {
expect(
serializePolicy(
legacySerializePolicy(
{
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'none',
@ -290,17 +248,6 @@ describe('Policy serialization', () => {
// There should be no allocation action in any phases...
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
migrate: {
@ -330,7 +277,6 @@ describe('Policy serialization', () => {
const originalPolicy = {
name: 'test',
phases: {
hot: { actions: {} },
warm: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
@ -345,7 +291,6 @@ describe('Policy serialization', () => {
const deserializedPolicy = {
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'none' as DataTierAllocationType,
@ -363,26 +308,20 @@ describe('Policy serialization', () => {
},
};
serializePolicy(deserializedPolicy, originalPolicy);
legacySerializePolicy(deserializedPolicy, originalPolicy);
deserializedPolicy.phases.warm.dataTierAllocationType = 'custom';
serializePolicy(deserializedPolicy, originalPolicy);
legacySerializePolicy(deserializedPolicy, originalPolicy);
deserializedPolicy.phases.warm.dataTierAllocationType = 'default';
serializePolicy(deserializedPolicy, originalPolicy);
legacySerializePolicy(deserializedPolicy, originalPolicy);
expect(originalPolicy).toEqual(originalClone);
});
test('serialize a policy using "best_compression" codec for forcemerge', () => {
expect(
serializePolicy(
legacySerializePolicy(
{
name: 'test',
phases: {
hot: {
...defaultNewHotPhase,
forceMergeEnabled: true,
selectedForceMergeSegments: '1',
bestCompressionEnabled: true,
},
warm: {
...defaultNewWarmPhase,
phaseEnabled: true,
@ -406,21 +345,6 @@ describe('Policy serialization', () => {
).toEqual({
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
forcemerge: {
max_num_segments: 1,
index_codec: 'best_compression',
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
forcemerge: {
@ -477,12 +401,6 @@ describe('Policy serialization', () => {
).toEqual({
name: 'test',
phases: {
hot: {
...defaultNewHotPhase,
forceMergeEnabled: true,
selectedForceMergeSegments: '1',
bestCompressionEnabled: true,
},
warm: {
...defaultNewWarmPhase,
warmPhaseOnRollover: false,
@ -501,16 +419,10 @@ describe('Policy serialization', () => {
test('delete "best_compression" codec for forcemerge if disabled in UI', () => {
expect(
serializePolicy(
legacySerializePolicy(
{
name: 'test',
phases: {
hot: {
...defaultNewHotPhase,
forceMergeEnabled: true,
selectedForceMergeSegments: '1',
bestCompressionEnabled: false,
},
warm: {
...defaultNewWarmPhase,
phaseEnabled: true,
@ -527,7 +439,6 @@ describe('Policy serialization', () => {
{
name: 'test',
phases: {
hot: { actions: {} },
warm: {
actions: {
forcemerge: {
@ -542,20 +453,6 @@ describe('Policy serialization', () => {
).toEqual({
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
forcemerge: {
max_num_segments: 1,
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
forcemerge: {

View file

@ -4,17 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Policy, PolicyFromES, SerializedPolicy } from '../../../../common/types';
import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types';
import {
defaultNewColdPhase,
defaultNewDeletePhase,
defaultNewHotPhase,
defaultNewWarmPhase,
serializedPhaseInitialization,
} from '../../constants';
import { hotPhaseFromES, hotPhaseToES } from './hot_phase';
import { warmPhaseFromES, warmPhaseToES } from './warm_phase';
import { coldPhaseFromES, coldPhaseToES } from './cold_phase';
import { deletePhaseFromES, deletePhaseToES } from './delete_phase';
@ -46,11 +44,10 @@ export const getPolicyByName = (
}
};
export const initializeNewPolicy = (newPolicyName: string = ''): Policy => {
export const initializeNewPolicy = (newPolicyName: string = ''): LegacyPolicy => {
return {
name: newPolicyName,
phases: {
hot: { ...defaultNewHotPhase },
warm: { ...defaultNewWarmPhase },
cold: { ...defaultNewColdPhase },
delete: { ...defaultNewDeletePhase },
@ -58,7 +55,7 @@ export const initializeNewPolicy = (newPolicyName: string = ''): Policy => {
};
};
export const deserializePolicy = (policy: PolicyFromES): Policy => {
export const deserializePolicy = (policy: PolicyFromES): LegacyPolicy => {
const {
name,
policy: { phases },
@ -67,7 +64,6 @@ export const deserializePolicy = (policy: PolicyFromES): Policy => {
return {
name,
phases: {
hot: hotPhaseFromES(phases.hot),
warm: warmPhaseFromES(phases.warm),
cold: coldPhaseFromES(phases.cold),
delete: deletePhaseFromES(phases.delete),
@ -75,8 +71,8 @@ export const deserializePolicy = (policy: PolicyFromES): Policy => {
};
};
export const serializePolicy = (
policy: Policy,
export const legacySerializePolicy = (
policy: LegacyPolicy,
originalEsPolicy: SerializedPolicy = {
name: policy.name,
phases: { hot: { ...serializedPhaseInitialization } },
@ -84,7 +80,7 @@ export const serializePolicy = (
): SerializedPolicy => {
const serializedPolicy = {
name: policy.name,
phases: { hot: hotPhaseToES(policy.phases.hot, originalEsPolicy.phases.hot) },
phases: {},
} as SerializedPolicy;
if (policy.phases.warm.phaseEnabled) {
serializedPolicy.phases.warm = warmPhaseToES(policy.phases.warm, originalEsPolicy.phases.warm);

View file

@ -8,12 +8,10 @@ import { i18n } from '@kbn/i18n';
import {
ColdPhase,
DeletePhase,
HotPhase,
Policy,
LegacyPolicy,
PolicyFromES,
WarmPhase,
} from '../../../../common/types';
import { validateHotPhase } from './hot_phase';
import { validateWarmPhase } from './warm_phase';
import { validateColdPhase } from './cold_phase';
import { validateDeletePhase } from './delete_phase';
@ -35,27 +33,6 @@ export const positiveNumberRequiredMessage = i18n.translate(
}
);
export const maximumAgeRequiredMessage = i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError',
{
defaultMessage: 'A maximum age is required.',
}
);
export const maximumSizeRequiredMessage = i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError',
{
defaultMessage: 'A maximum index size is required.',
}
);
export const maximumDocumentsRequiredMessage = i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError',
{
defaultMessage: 'Maximum documents is required.',
}
);
export const positiveNumbersAboveZeroErrorMessage = i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError',
{
@ -112,7 +89,6 @@ export type PhaseValidationErrors<T> = {
};
export interface ValidationErrors {
hot: PhaseValidationErrors<HotPhase>;
warm: PhaseValidationErrors<WarmPhase>;
cold: PhaseValidationErrors<ColdPhase>;
delete: PhaseValidationErrors<DeletePhase>;
@ -121,7 +97,7 @@ export interface ValidationErrors {
export const validatePolicy = (
saveAsNew: boolean,
policy: Policy,
policy: LegacyPolicy,
policies: PolicyFromES[],
originalPolicyName: string
): [boolean, ValidationErrors] => {
@ -152,13 +128,11 @@ export const validatePolicy = (
}
}
const hotPhaseErrors = validateHotPhase(policy.phases.hot);
const warmPhaseErrors = validateWarmPhase(policy.phases.warm);
const coldPhaseErrors = validateColdPhase(policy.phases.cold);
const deletePhaseErrors = validateDeletePhase(policy.phases.delete);
const isValid =
policyNameErrors.length === 0 &&
Object.keys(hotPhaseErrors).length === 0 &&
Object.keys(warmPhaseErrors).length === 0 &&
Object.keys(coldPhaseErrors).length === 0 &&
Object.keys(deletePhaseErrors).length === 0;
@ -166,7 +140,6 @@ export const validatePolicy = (
isValid,
{
policyName: [...policyNameErrors],
hot: hotPhaseErrors,
warm: warmPhaseErrors,
cold: coldPhaseErrors,
delete: deletePhaseErrors,
@ -183,9 +156,6 @@ export const findFirstError = (errors?: ValidationErrors): string | undefined =>
return propertyof<ValidationErrors>('policyName');
}
if (Object.keys(errors.hot).length > 0) {
return `${propertyof<ValidationErrors>('hot')}.${Object.keys(errors.hot)[0]}`;
}
if (Object.keys(errors.warm).length > 0) {
return `${propertyof<ValidationErrors>('warm')}.${Object.keys(errors.warm)[0]}`;
}

View file

@ -14,8 +14,8 @@ import {
UIM_CONFIG_SET_PRIORITY,
UIM_CONFIG_WARM_PHASE,
defaultNewColdPhase,
defaultNewHotPhase,
defaultNewWarmPhase,
defaultSetPriority,
} from '../constants';
import { Phases } from '../../../common/types';
@ -45,8 +45,7 @@ export function getUiMetricsForPhases(phases: Phases): string[] {
const isHotPhasePriorityChanged =
phases.hot &&
phases.hot.actions.set_priority &&
phases.hot.actions.set_priority.priority !==
parseInt(defaultNewHotPhase.phaseIndexPriority, 10);
phases.hot.actions.set_priority.priority !== parseInt(defaultSetPriority, 10);
const isWarmPhasePriorityChanged =
phases.warm &&

View file

@ -143,7 +143,7 @@ export class IndexLifecycleSummary extends Component<Props, State> {
);
return (
<Fragment key="phaseDefinition">
<EuiDescriptionListTitle>
<EuiDescriptionListTitle key="phaseDefinition_title">
<strong>
<FormattedMessage
defaultMessage="Phase definition"
@ -204,14 +204,14 @@ export class IndexLifecycleSummary extends Component<Props, State> {
}
content = content || '-';
const cell = (
<>
<Fragment key={String(arrayIndex)}>
<EuiDescriptionListTitle key={fieldName}>
<strong>{label}</strong>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription key={fieldName + '_desc'}>
{content}
</EuiDescriptionListDescription>
</>
</Fragment>
);
if (arrayIndex % 2 === 0) {
rows.left.push(cell);

View file

@ -3,8 +3,31 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AppServicesContext } from './types';
import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public';
export {
useForm,
useFormData,
Form,
UseField,
FieldConfig,
OnFormUpdateArg,
ValidationFunc,
getFieldValidityAndErrorMessage,
useFormContext,
FormSchema,
} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers';
export {
ToggleField,
NumericField,
SelectField,
} from '../../../../src/plugins/es_ui_shared/static/forms/components';
export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public';
export const useKibana = () => _useKibana<AppServicesContext>();