From 580264fe1ab9523eaaa683a20f162b7bda8a18a6 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 20 Oct 2020 09:55:32 +0200 Subject: [PATCH] [ILM] Migrate Hot phase to Form Lib (#80012) (#81013) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/fields/checkbox_field.tsx | 2 +- .../components/fields/combobox_field.tsx | 2 +- .../components/fields/multi_select_field.tsx | 2 +- .../forms/components/fields/numeric_field.tsx | 2 +- .../components/fields/radio_group_field.tsx | 2 +- .../forms/components/fields/range_field.tsx | 2 +- .../forms/components/fields/select_field.tsx | 2 +- .../components/fields/super_select_field.tsx | 2 +- .../components/fields/text_area_field.tsx | 2 +- .../forms/components/fields/text_field.tsx | 2 +- .../forms/components/fields/toggle_field.tsx | 2 +- .../extend_index_management.test.tsx.snap | 4 +- .../edit_policy/constants.ts | 21 + .../edit_policy/edit_policy.helpers.tsx | 95 +++- .../edit_policy/edit_policy.test.ts | 84 ++- .../client_integration/helpers/index.ts | 10 +- .../__jest__/components/edit_policy.test.tsx | 286 ++++++---- .../common/types/policies.ts | 15 +- .../public/application/constants/index.ts | 1 + .../public/application/constants/policy.ts | 28 +- .../edit_policy/components/forcemerge.tsx | 5 + .../edit_policy/components/form_errors.tsx | 9 +- .../sections/edit_policy/components/index.ts | 2 + .../edit_policy/components/min_age_input.tsx | 2 +- .../{ => components}/phases/cold_phase.tsx | 17 +- .../components/phases/delete_phase.tsx | 155 ++++++ .../components/phases/hot_phase/constants.ts | 91 ++++ .../components/phases/hot_phase/hot_phase.tsx | 235 ++++++++ .../components/phases/hot_phase/i18n_texts.ts | 37 ++ .../phases/hot_phase}/index.ts | 2 +- .../{ => components}/phases/index.ts | 3 + .../shared/data_tier_allocation_field.tsx | 12 +- .../phases/shared/forcemerge_field.tsx | 95 ++++ .../components/phases/shared/index.ts | 13 + .../phases/shared/set_priority_input.tsx | 62 +++ .../{ => components}/phases/warm_phase.tsx | 18 +- .../components/policy_json_flyout.tsx | 130 +++-- .../components/set_priority_input.tsx | 6 + .../sections/edit_policy/constants.ts | 17 + .../sections/edit_policy/deserializer.ts | 43 ++ .../sections/edit_policy/edit_policy.tsx | 500 ++++++++++-------- .../sections/edit_policy/form_schema.ts | 129 +++++ .../sections/edit_policy/form_validations.ts | 73 +++ .../sections/edit_policy/i18n_texts.ts | 15 + .../edit_policy/phases/delete_phase.tsx | 153 ------ .../sections/edit_policy/phases/hot_phase.tsx | 336 ------------ .../sections/edit_policy/serializer.ts | 43 ++ .../application/sections/edit_policy/types.ts | 26 + .../services/policies/hot_phase.ts | 190 ------- .../services/policies/policy_save.ts | 15 +- .../policies/policy_serialization.test.ts | 125 +---- .../services/policies/policy_serialization.ts | 16 +- .../services/policies/policy_validation.ts | 34 +- .../public/application/services/ui_metric.ts | 5 +- .../components/index_lifecycle_summary.tsx | 6 +- .../public/shared_imports.ts | 23 + 56 files changed, 1907 insertions(+), 1302 deletions(-) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{ => components}/phases/cold_phase.tsx (95%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{phases/shared => components/phases/hot_phase}/index.ts (77%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{ => components}/phases/index.ts (99%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{ => components}/phases/shared/data_tier_allocation_field.tsx (90%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/set_priority_input.tsx rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{ => components}/phases/warm_phase.tsx (96%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx index c8ba9f5ac4102..b80d6caf54f4f 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx @@ -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} > error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > { error={errorMessage} isInvalid={isInvalid} fullWidth - data-test-subj={rest['data-test-subj']} describedByIds={rest.idAria ? [rest.idAria] : undefined} + {...rest} > - +
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index 4dff70518c115..bd845b0a7d9a7 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -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: { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 6365bb8caa963..0cfccba761309 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -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(EditPolicy, testBedConfig); -export interface EditPolicyTestBed extends TestBed { - actions: { - setWaitForSnapshotPolicy: (snapshotPolicyName: string) => void; - savePolicy: () => void; - }; -} +type SetupReturn = ReturnType; + +export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; -export const setup = async (): Promise => { +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'), + }, }, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index b465afb8b5d9f..3cbc2d982566e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -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('', () => { 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]); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index 7b227f822fa97..c6d27ca890b54 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -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; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index 953e6244b077f..d9af20763657b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -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 = ( @@ -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 = ( { 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/\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(); diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index f7c1b810ecbc5..152c5e4e9e0d7 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -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, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts index 61c197f2ba149..53f6b30a01c39 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/index.ts @@ -5,4 +5,5 @@ */ export * from './policy'; + export * from './ui_metric'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 1cd81b55ea173..c919331082ec3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -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 = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx index c079c7a19ce91..0b0dbe273c024 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx @@ -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, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx index 9db40ebf5521f..ed7ca60417679 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx @@ -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 = ({ @@ -18,8 +18,13 @@ export const ErrableFormRow: React.FunctionComponent = ({ children, ...rest }) => { + const _errors = errors ? (Array.isArray(errors) ? errors : [errors]) : undefined; return ( - 0} error={errors} {...rest}> + 0)} + error={errors} + {...rest} + > {Children.map(children, (child) => cloneElement(child as ReactElement, { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index c39545112ee52..04d9a6ef3cf67 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -22,3 +22,5 @@ export { } from './data_tier_allocation'; export { DescribedFormField } from './described_form_field'; export { Forcemerge } from './forcemerge'; + +export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index 2e70ef255524d..6fcf35b799289 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -212,7 +212,7 @@ export const MinAgeInput = ({ { + onChange={(e) => { setPhaseData(selectedMinimumAgeProperty, e.target.value); }} min={0} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx similarity index 95% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx index b9b1b8b663ec8..7ed8a94403a9b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx @@ -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; - hotPhaseRolloverEnabled: boolean; } export const ColdPhase: FunctionComponent = ({ setPhaseData, phaseData, errors, isShowingErrors, - hotPhaseRolloverEnabled, }) => { + const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({ + watch: [useRolloverPath], + }); + return (
<> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx new file mode 100644 index 0000000000000..59e4738657be4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx @@ -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; + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; +} + +export const DeletePhase: FunctionComponent = ({ + setPhaseData, + phaseData, + errors, + isShowingErrors, + getUrlForApp, +}) => { + const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({ + watch: [useRolloverPath], + }); + + return ( +
+ +

+ +

{' '} + {phaseData.phaseEnabled && !isShowingErrors ? : null} + +
+ } + titleSize="s" + description={ + +

+ +

+ + } + id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} + onChange={(e) => { + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); + }} + aria-controls="deletePhaseContent" + /> +
+ } + fullWidth + > + {phaseData.phaseEnabled ? ( + + errors={errors} + phaseData={phaseData} + phase={deleteProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + ) : ( +
+ )} + + {phaseData.phaseEnabled ? ( + + + + } + description={ + + {' '} + + + } + titleSize="xs" + fullWidth + > + + + + + } + > + setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)} + getUrlForApp={getUrlForApp} + /> + + + ) : null} +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts new file mode 100644 index 0000000000000..e438fa470c0c4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/constants.ts @@ -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', + }), + }, +]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx new file mode 100644 index 0000000000000..ae84c0afe975f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -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 ( + <> + +

+ +

{' '} + {isShowingErrors ? null : } + +
+ } + titleSize="s" + description={ + +

+ +

+
+ } + fullWidth + > + + key="_meta.hot.useRollover" + path="_meta.hot.useRollover" + component={ToggleField} + componentProps={{ + hasEmptyLabelSpace: true, + fullWidth: false, + helpText: ( + <> +

+ +

+ + } + docPath="indices-rollover-index.html" + /> + + + ), + euiFieldProps: { + 'data-test-subj': 'rolloverSwitch', + }, + }} + /> + {isRolloverEnabled && ( + <> + + {showEmptyRolloverFieldsError && ( + <> + +
{i18nTexts.rollOverConfigurationCallout.body}
+
+ + + )} + + + + {(field) => { + const showErrorCallout = field.errors.some( + (e) => e.validationType === ROLLOVER_EMPTY_VALIDATION + ); + if (showErrorCallout !== showEmptyRolloverFieldsError) { + setShowEmptyRolloverFieldsError(showErrorCallout); + } + return ( + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + )} + + {isRolloverEnabled && } + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts new file mode 100644 index 0000000000000..6423b12b86dd2 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts @@ -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.', + }), + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/index.ts similarity index 77% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/index.ts index f9e939058adb9..325e35f985932 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/index.ts @@ -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'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts similarity index 99% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts index 8d1ace5950497..076c16e87e8d6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts @@ -5,6 +5,9 @@ */ export { HotPhase } from './hot_phase'; + export { WarmPhase } from './warm_phase'; + export { ColdPhase } from './cold_phase'; + export { DeletePhase } from './delete_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx similarity index 90% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx index b3772a6e3ebd4..de7f321e5f15d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx @@ -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', { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx new file mode 100644 index 0000000000000..987133fd652ac --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx @@ -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 = ({ phase }) => { + const [{ [forceMergeEnabledPath]: forceMergeEnabled }] = useFormData({ + watch: [forceMergeEnabledPath], + }); + return ( + + + + } + description={ + + {' '} + + + } + titleSize="xs" + fullWidth + > + + +
+ {forceMergeEnabled && ( + <> + + + + )} +
+
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts new file mode 100644 index 0000000000000..3b94d36a977d1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts @@ -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'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/set_priority_input.tsx new file mode 100644 index 0000000000000..0f7ca4d52f889 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/set_priority_input.tsx @@ -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 = ({ phase }) => { + const phaseIndexPriorityProperty = propertyof('phaseIndexPriority'); + return ( + + + + } + description={ + + {' '} + + + } + titleSize="xs" + fullWidth + > + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase.tsx similarity index 96% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase.tsx index b837eed1256c5..bd0b380bdc172 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase.tsx @@ -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; - hotPhaseRolloverEnabled: boolean; } export const WarmPhase: FunctionComponent = ({ setPhaseData, phaseData, errors, isShowingErrors, - hotPhaseRolloverEnabled, }) => { + const [{ [useRolloverPath]: hotPhaseRolloverEnabled }] = useFormData({ + watch: [useRolloverPath], + }); return (
<> @@ -132,7 +138,7 @@ export const WarmPhase: FunctionComponent = ({ /> ) : null} - {!phaseData.warmPhaseOnRollover ? ( + {!phaseData.warmPhaseOnRollover || !hotPhaseRolloverEnabled ? ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx index 98d2409ffea6d..e9ce193118b35 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -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 = ({ - 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); + + 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]); - const endpoint = `PUT _ilm/policy/${policyName || ''}`; - const request = `${endpoint}\n${json}`; + let content: React.ReactNode; + switch (policy) { + case undefined: + content = ; + break; + case null: + content = ( + + {i18n.translate('xpack.indexLifecycleMgmt.policyJsonFlyout.validationErrorCallout.body', { + defaultMessage: 'To view the JSON for this policy address all validation errors.', + })} + + ); + break; + default: + const { phases } = policy; + + const json = JSON.stringify( + { + policy: { + phases, + }, + }, + null, + 2 + ); + + const endpoint = `PUT _ilm/policy/${policyName || ''}`; + const request = `${endpoint}\n${json}`; + content = ( + <> + +

+ +

+
+ + + {request} + + + ); + break; + } return ( @@ -69,22 +142,7 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({ - - -

- -

-
- - - - - {request} - -
+ {content} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index 7f839fc94918b..5efbfabdf093d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -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'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts new file mode 100644 index 0000000000000..a5d5f1c62847c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts @@ -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', +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts new file mode 100644 index 0000000000000..bb24eea64ec8c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts @@ -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( + { + ...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; + } + } + } + ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 67e8e42cf6fd1..8f8b0447f378a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -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 = ({ policies, policyName, @@ -73,7 +109,18 @@ export const EditPolicy: React.FunctionComponent = ({ const existingPolicy = getPolicyByName(policies, policyName); - const [policy, setPolicy] = useState( + const serializer = useMemo(() => { + return createSerializer(existingPolicy?.policy); + }, [existingPolicy?.policy]); + + const { form } = useForm({ + schema, + defaultValue: existingPolicy?.policy ?? defaultPolicy, + deserializer, + serializer, + }); + + const [policy, setPolicy] = useState(() => existingPolicy ? deserializePolicy(existingPolicy) : initializeNewPolicy(policyName) ); @@ -85,9 +132,26 @@ export const EditPolicy: React.FunctionComponent = ({ 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 = ({ ); 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 = ({ }; 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 = ({ [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 = ({ [setPhaseData] ); - const setWarmPhaseOnRollover = (value: boolean) => { - setPolicy({ - ...policy, - phases: { - ...policy.phases, - hot: { - ...policy.phases.hot, - rolloverEnabled: value, - }, - warm: { - ...policy.phases.warm, - warmPhaseOnRollover: value, - }, - }, - }); - }; - return ( @@ -188,215 +241,210 @@ export const EditPolicy: React.FunctionComponent = ({
- - -

- + +

+ {' '} - - } - /> -

-
+ />{' '} + + } + /> +

+ - + - {isNewPolicy ? null : ( - - -

- + {isNewPolicy ? null : ( + + +

+ + + + .{' '} - - .{' '} - +

+
+ + + + { + setSaveAsNew(e.target.checked); + }} + label={ + + + + } /> -

- - - - - { - setSaveAsNew(e.target.checked); - }} - label={ - + +
+ )} + + {saveAsNew || isNewPolicy ? ( + + - } - /> - - - )} - - {saveAsNew || isNewPolicy ? ( - - +
+ } + titleSize="s" + fullWidth + > + - -
- } - titleSize="s" - fullWidth - > - + { + setPolicy({ ...policy, name: e.target.value }); + }} /> - } - > - { - setPolicy({ ...policy, name: e.target.value }); - }} - /> - - - ) : null} - - - - 0} - setPhaseData={setHotPhaseData} - phaseData={policy.phases.hot} - setWarmPhaseOnRollover={setWarmPhaseOnRollover} - /> - - - - 0} - setPhaseData={setWarmPhaseData} - phaseData={policy.phases.warm} - hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} - /> - - - - 0} - setPhaseData={setColdPhaseData} - phaseData={policy.phases.cold} - hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} - /> - - - - 0} - getUrlForApp={getUrlForApp} - setPhaseData={setDeletePhaseData} - phaseData={policy.phases.delete} - hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} - /> - - - - - - - - - {saveAsNew ? ( - - ) : ( + + + ) : null} + + + + + + + + 0} + setPhaseData={setWarmPhaseData} + phaseData={policy.phases.warm} + /> + + + + 0} + setPhaseData={setColdPhaseData} + phaseData={policy.phases.cold} + /> + + + + 0 + } + getUrlForApp={getUrlForApp} + setPhaseData={setDeletePhaseData} + phaseData={policy.phases.delete} + /> + + + + + + + + + {saveAsNew ? ( + + ) : ( + + )} + + + + + - )} - - - - - + + + + + + + + {isShowingPolicyJsonFlyout ? ( - - - - - - - - {isShowingPolicyJsonFlyout ? ( - - ) : ( - - )} - - - - - {isShowingPolicyJsonFlyout ? ( - setIsShowingPolicyJsonFlyout(false)} - /> - ) : null} + ) : ( + + )} + + + + + {isShowingPolicyJsonFlyout ? ( + setIsShowingPolicyJsonFlyout(false)} + /> + ) : null} + diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts new file mode 100644 index 0000000000000..806164c8b0da1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts @@ -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 = { + _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), + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts new file mode 100644 index 0000000000000..b937ea2043138 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts @@ -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 = (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); + } +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts new file mode 100644 index 0000000000000..31bb10b402d27 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -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', + }), + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx deleted file mode 100644 index 11adebdd094bf..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx +++ /dev/null @@ -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; - hotPhaseRolloverEnabled: boolean; - getUrlForApp: ( - appId: string, - options?: { - path?: string; - absolute?: boolean; - } - ) => string; -} - -export class DeletePhase extends PureComponent { - render() { - const { - setPhaseData, - phaseData, - errors, - isShowingErrors, - hotPhaseRolloverEnabled, - getUrlForApp, - } = this.props; - - return ( -
- -

- -

{' '} - {phaseData.phaseEnabled && !isShowingErrors ? : null} - -
- } - titleSize="s" - description={ - -

- -

- - } - id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`} - checked={phaseData.phaseEnabled} - onChange={(e) => { - setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); - }} - aria-controls="deletePhaseContent" - /> -
- } - fullWidth - > - {phaseData.phaseEnabled ? ( - - errors={errors} - phaseData={phaseData} - phase={deleteProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - rolloverEnabled={hotPhaseRolloverEnabled} - /> - ) : ( -
- )} - - {phaseData.phaseEnabled ? ( - - - - } - description={ - - {' '} - - - } - titleSize="xs" - fullWidth - > - - - - - } - > - setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)} - getUrlForApp={getUrlForApp} - /> - - - ) : null} -
- ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx deleted file mode 100644 index 7682b92488086..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ /dev/null @@ -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; - isShowingErrors: boolean; - phaseData: HotPhaseInterface; - setPhaseData: (key: keyof HotPhaseInterface & string, value: string | boolean) => void; - setWarmPhaseOnRollover: (value: boolean) => void; -} - -export class HotPhase extends PureComponent { - render() { - const { setPhaseData, phaseData, isShowingErrors, errors, setWarmPhaseOnRollover } = this.props; - - return ( - - -

- -

{' '} - {isShowingErrors ? null : } - - - } - titleSize="s" - description={ - -

- -

-
- } - fullWidth - > - -

- -

- - } - docPath="indices-rollover-index.html" - /> - -
- } - > - { - setWarmPhaseOnRollover(e.target.checked); - }} - label={i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { - defaultMessage: 'Enable rollover', - })} - /> -
- {phaseData.rolloverEnabled ? ( - - - - - - { - setPhaseData(phaseProperty('selectedMaxSizeStored'), e.target.value); - }} - min={1} - /> - - - - - { - setPhaseData(phaseProperty('selectedMaxSizeStoredUnits'), e.target.value); - }} - options={maxSizeStoredUnits} - /> - - - - - - - - { - setPhaseData(phaseProperty('selectedMaxDocuments'), e.target.value); - }} - min={1} - /> - - - - - - - - { - setPhaseData(phaseProperty('selectedMaxAge'), e.target.value); - }} - min={1} - /> - - - - - { - setPhaseData(phaseProperty('selectedMaxAgeUnits'), e.target.value); - }} - options={maxAgeUnits} - /> - - - - - ) : null} - - {phaseData.rolloverEnabled ? ( - - ) : null} - - errors={errors} - phaseData={phaseData} - phase={hotProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - /> - - ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts new file mode 100644 index 0000000000000..e0e1ad44f1557 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts @@ -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; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts new file mode 100644 index 0000000000000..dba56eb8ecbf3 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -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; + }; + }; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts deleted file mode 100644 index 3bb9165c7d4f0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts +++ /dev/null @@ -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 => { - if (!phase.phaseEnabled) { - return {}; - } - - const phaseErrors = {} as PhaseValidationErrors; - - // 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, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts index a96b6f57a0f9f..9cf622e830cb2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts @@ -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 => { - 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); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts index 71ae9b26e2903..5c7f04986827b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts @@ -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: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts index 996b2e8c371b8..0dce7efce4623 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -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); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts index ffd3c01ab001d..eeceb97c409f5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -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 = { }; export interface ValidationErrors { - hot: PhaseValidationErrors; warm: PhaseValidationErrors; cold: PhaseValidationErrors; delete: PhaseValidationErrors; @@ -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('policyName'); } - if (Object.keys(errors.hot).length > 0) { - return `${propertyof('hot')}.${Object.keys(errors.hot)[0]}`; - } if (Object.keys(errors.warm).length > 0) { return `${propertyof('warm')}.${Object.keys(errors.warm)[0]}`; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index aeb2c8ce917c6..ea5c5619da589 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -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 && diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx index ce36a3650c2ff..d711863c309e9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx @@ -143,7 +143,7 @@ export class IndexLifecycleSummary extends Component { ); return ( - + { } content = content || '-'; const cell = ( - <> + {label} {content} - + ); if (arrayIndex % 2 === 0) { rows.left.push(cell); diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts index d479b821ceefc..dc3e1b1d1b62d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -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();