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 e5037a6477aca..acf642f250a7b 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 @@ -4,21 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PolicyFromES } from '../../../public/application/services/policies/types'; + 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 DELETE_PHASE_POLICY = { +export const DELETE_PHASE_POLICY: PolicyFromES = { version: 1, - modified_date: Date.now(), + modified_date: Date.now().toString(), policy: { phases: { hot: { min_age: '0ms', actions: { - set_priority: { - priority: null, - }, rollover: { max_size: '50gb', }, @@ -36,6 +35,7 @@ export const DELETE_PHASE_POLICY = { }, }, }, + name: POLICY_NAME, }, name: POLICY_NAME, }; 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 ebe1c12e2a079..6365bb8caa963 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 @@ -13,7 +13,6 @@ import { POLICY_NAME } from './constants'; import { TestSubjects } from '../helpers'; import { EditPolicy } from '../../../public/application/sections/edit_policy'; -import { indexLifecycleManagementStore } from '../../../public/application/store'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -35,7 +34,6 @@ jest.mock('@elastic/eui', () => { }); const testBedConfig: TestBedConfig = { - store: () => indexLifecycleManagementStore(), memoryRouter: { initialEntries: [`/policies/edit/${POLICY_NAME}`], componentRoutePath: `/policies/edit/:policyName`, 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 06829e6ef6f1e..36feb3f6203c8 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 @@ -40,8 +40,8 @@ describe('', () => { test('wait for snapshot policy field should correctly display snapshot policy name', () => { expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ { - label: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, - value: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, + label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy, + value: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy, }, ]); }); @@ -59,7 +59,7 @@ describe('', () => { delete: { ...DELETE_PHASE_POLICY.policy.phases.delete, actions: { - ...DELETE_PHASE_POLICY.policy.phases.delete.actions, + ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, wait_for_snapshot: { policy: NEW_SNAPSHOT_POLICY_NAME, }, @@ -96,7 +96,7 @@ describe('', () => { delete: { ...DELETE_PHASE_POLICY.policy.phases.delete, actions: { - ...DELETE_PHASE_POLICY.policy.phases.delete.actions, + ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js index 4fe3d5c66696e..81c30579cd4dd 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js @@ -7,7 +7,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import moment from 'moment-timezone'; -import { Provider } from 'react-redux'; // axios has a $http like interface so using it to simulate $http import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; @@ -21,9 +20,7 @@ import { import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies } from '../../public/application/store/actions'; -import { indexLifecycleManagementStore } from '../../public/application/store'; -import { EditPolicy } from '../../public/application/sections/edit_policy'; +import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy'; import { init as initHttp } from '../../public/application/services/http'; import { init as initUiMetric } from '../../public/application/services/ui_metric'; import { init as initNotification } from '../../public/application/services/notification'; @@ -40,7 +37,7 @@ import { policyNameMustBeDifferentErrorMessage, policyNameAlreadyUsedErrorMessage, maximumDocumentsRequiredMessage, -} from '../../public/application/store/selectors'; +} from '../../public/application/services/policies/policy_validation'; initHttp(axios.create({ adapter: axiosXhrAdapter })); initUiMetric(usageCollectionPluginMock.createSetupContract()); @@ -51,7 +48,6 @@ initNotification( let server; let httpRequestsMockHelpers; -let store; const policy = { phases: { hot: { @@ -128,13 +124,14 @@ const save = (rendered) => { }; describe('edit policy', () => { beforeEach(() => { - store = indexLifecycleManagementStore(); component = ( - - {} }} getUrlForApp={() => {}} /> - + {} }} + getUrlForApp={() => {}} + policies={policies} + policyName={''} + /> ); - store.dispatch(fetchedPolicies(policies)); ({ server, httpRequestsMockHelpers } = initHttpRequests()); httpRequestsMockHelpers.setPoliciesResponse(policies); @@ -162,9 +159,12 @@ describe('edit policy', () => { }); test('should show error when trying to save as new policy but using the same name', () => { component = ( - - {}} /> - + {}} + history={{ push: () => {} }} + /> ); const rendered = mountWithIntl(component); findTestSubject(rendered, 'saveAsNewSwitch').simulate('click'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx index 14b0e72317c66..f7f8b30324bca 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx @@ -9,7 +9,7 @@ import { Router, Switch, Route, Redirect } from 'react-router-dom'; import { ScopedHistory, ApplicationStart } from 'kibana/public'; import { METRIC_TYPE } from '@kbn/analytics'; -import { UIM_APP_LOAD } from './constants'; +import { UIM_APP_LOAD } from './constants/ui_metric'; import { EditPolicy } from './sections/edit_policy'; import { PolicyTable } from './sections/policy_table'; import { trackUiMetric } from './services/ui_metric'; 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 6319fc0d68543..61c197f2ba149 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 @@ -4,102 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './policy'; export * from './ui_metric'; - -export const SET_PHASE_DATA: string = 'SET_PHASE_DATA'; -export const SET_SELECTED_NODE_ATTRS: string = 'SET_SELECTED_NODE_ATTRS'; -export const PHASE_HOT: string = 'hot'; -export const PHASE_WARM: string = 'warm'; -export const PHASE_COLD: string = 'cold'; -export const PHASE_DELETE: string = 'delete'; - -export const PHASE_ENABLED: string = 'phaseEnabled'; - -export const PHASE_ROLLOVER_ENABLED: string = 'rolloverEnabled'; -export const WARM_PHASE_ON_ROLLOVER: string = 'warmPhaseOnRollover'; -export const PHASE_ROLLOVER_ALIAS: string = 'selectedAlias'; -export const PHASE_ROLLOVER_MAX_AGE: string = 'selectedMaxAge'; -export const PHASE_ROLLOVER_MAX_AGE_UNITS: string = 'selectedMaxAgeUnits'; -export const PHASE_ROLLOVER_MAX_SIZE_STORED: string = 'selectedMaxSizeStored'; -export const PHASE_ROLLOVER_MAX_DOCUMENTS: string = 'selectedMaxDocuments'; -export const PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS: string = 'selectedMaxSizeStoredUnits'; -export const PHASE_ROLLOVER_MINIMUM_AGE: string = 'selectedMinimumAge'; -export const PHASE_ROLLOVER_MINIMUM_AGE_UNITS: string = 'selectedMinimumAgeUnits'; - -export const PHASE_FORCE_MERGE_SEGMENTS: string = 'selectedForceMergeSegments'; -export const PHASE_FORCE_MERGE_ENABLED: string = 'forceMergeEnabled'; -export const PHASE_FREEZE_ENABLED: string = 'freezeEnabled'; - -export const PHASE_SHRINK_ENABLED: string = 'shrinkEnabled'; - -export const PHASE_NODE_ATTRS: string = 'selectedNodeAttrs'; -export const PHASE_PRIMARY_SHARD_COUNT: string = 'selectedPrimaryShardCount'; -export const PHASE_REPLICA_COUNT: string = 'selectedReplicaCount'; -export const PHASE_INDEX_PRIORITY: string = 'phaseIndexPriority'; - -export const PHASE_WAIT_FOR_SNAPSHOT_POLICY = 'waitForSnapshotPolicy'; - -export const PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE: string[] = [ - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_INDEX_PRIORITY, -]; -export const PHASE_ATTRIBUTES_THAT_ARE_NUMBERS: string[] = [ - ...PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_DOCUMENTS, -]; - -export const STRUCTURE_INDEX_TEMPLATE: string = 'indexTemplate'; -export const STRUCTURE_TEMPLATE_SELECTION: string = 'templateSelection'; -export const STRUCTURE_TEMPLATE_NAME: string = 'templateName'; -export const STRUCTURE_CONFIGURATION: string = 'configuration'; -export const STRUCTURE_NODE_ATTRS: string = 'node_attrs'; -export const STRUCTURE_PRIMARY_NODES: string = 'primary_nodes'; -export const STRUCTURE_REPLICAS: string = 'replicas'; - -export const STRUCTURE_POLICY_CONFIGURATION: string = 'policyConfiguration'; - -export const STRUCTURE_REVIEW: string = 'review'; -export const STRUCTURE_POLICY_NAME: string = 'policyName'; -export const STRUCTURE_INDEX_NAME: string = 'indexName'; -export const STRUCTURE_ALIAS_NAME: string = 'aliasName'; - -export const ERROR_STRUCTURE: any = { - [PHASE_HOT]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MAX_AGE]: [], - [PHASE_ROLLOVER_MAX_AGE_UNITS]: [], - [PHASE_ROLLOVER_MAX_SIZE_STORED]: [], - [PHASE_ROLLOVER_MAX_DOCUMENTS]: [], - [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: [], - [PHASE_INDEX_PRIORITY]: [], - }, - [PHASE_WARM]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MINIMUM_AGE]: [], - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [], - [PHASE_NODE_ATTRS]: [], - [PHASE_PRIMARY_SHARD_COUNT]: [], - [PHASE_REPLICA_COUNT]: [], - [PHASE_FORCE_MERGE_SEGMENTS]: [], - [PHASE_INDEX_PRIORITY]: [], - }, - [PHASE_COLD]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MINIMUM_AGE]: [], - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [], - [PHASE_NODE_ATTRS]: [], - [PHASE_REPLICA_COUNT]: [], - [PHASE_INDEX_PRIORITY]: [], - }, - [PHASE_DELETE]: { - [PHASE_ROLLOVER_ALIAS]: [], - [PHASE_ROLLOVER_MINIMUM_AGE]: [], - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: [], - }, - [STRUCTURE_POLICY_NAME]: [], -}; 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 new file mode 100644 index 0000000000000..3a19f03547b5b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -0,0 +1,60 @@ +/* + * 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 { + SerializedPhase, + ColdPhase, + DeletePhase, + HotPhase, + WarmPhase, +} from '../services/policies/types'; + +export const defaultNewHotPhase: HotPhase = { + phaseEnabled: true, + rolloverEnabled: true, + selectedMaxAge: '30', + selectedMaxAgeUnits: 'd', + selectedMaxSizeStored: '50', + selectedMaxSizeStoredUnits: 'gb', + phaseIndexPriority: '100', + selectedMaxDocuments: '', +}; + +export const defaultNewWarmPhase: WarmPhase = { + phaseEnabled: false, + forceMergeEnabled: false, + selectedForceMergeSegments: '', + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + shrinkEnabled: false, + selectedPrimaryShardCount: '', + selectedReplicaCount: '', + warmPhaseOnRollover: true, + phaseIndexPriority: '50', +}; + +export const defaultNewColdPhase: ColdPhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + freezeEnabled: false, + phaseIndexPriority: '0', +}; + +export const defaultNewDeletePhase: DeletePhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + waitForSnapshotPolicy: '', +}; + +export const serializedPhaseInitialization: SerializedPhase = { + min_age: '0ms', + actions: {}, +}; 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 a3278b6c231b9..9db40ebf5521f 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 @@ -8,28 +8,22 @@ import React, { cloneElement, Children, Fragment, ReactElement } from 'react'; import { EuiFormRow, EuiFormRowProps } from '@elastic/eui'; type Props = EuiFormRowProps & { - errorKey: string; isShowingErrors: boolean; - errors: Record; + errors?: string[]; }; export const ErrableFormRow: React.FunctionComponent = ({ - errorKey, isShowingErrors, errors, children, ...rest }) => { return ( - 0} - error={errors[errorKey]} - {...rest} - > + 0} error={errors} {...rest}> {Children.map(children, (child) => cloneElement(child as ReactElement, { - isInvalid: isShowingErrors && errors[errorKey].length > 0, + isInvalid: errors && isShowingErrors && errors.length > 0, }) )} 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 c9732f2311758..11b743ecc4bb6 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 @@ -9,40 +9,35 @@ import { i18n } from '@kbn/i18n'; import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; -import { - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, -} from '../../../constants'; import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { ColdPhase, DeletePhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; -function getTimingLabelForPhase(phase: string) { +function getTimingLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { - case PHASE_WARM: + case 'warm': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel', { defaultMessage: 'Timing for warm phase', }); - case PHASE_COLD: + case 'cold': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel', { defaultMessage: 'Timing for cold phase', }); - case PHASE_DELETE: + case 'delete': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', { defaultMessage: 'Timing for delete phase', }); } } -function getUnitsAriaLabelForPhase(phase: string) { +function getUnitsAriaLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { - case PHASE_WARM: + case 'warm': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel', { @@ -50,7 +45,7 @@ function getUnitsAriaLabelForPhase(phase: string) { } ); - case PHASE_COLD: + case 'cold': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel', { @@ -58,7 +53,7 @@ function getUnitsAriaLabelForPhase(phase: string) { } ); - case PHASE_DELETE: + case 'delete': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel', { @@ -68,24 +63,23 @@ function getUnitsAriaLabelForPhase(phase: string) { } } -interface Props { +interface Props { rolloverEnabled: boolean; - errors: Record; - phase: string; - // TODO add types for phaseData and setPhaseData after policy is typed - phaseData: any; - setPhaseData: (dataKey: string, value: any) => void; + errors?: PhaseValidationErrors; + phase: keyof Phases & string; + phaseData: T; + setPhaseData: (dataKey: keyof T & string, value: string) => void; isShowingErrors: boolean; } -export const MinAgeInput: React.FunctionComponent = ({ +export const MinAgeInput = ({ rolloverEnabled, errors, phaseData, phase, setPhaseData, isShowingErrors, -}) => { +}: React.PropsWithChildren>): React.ReactElement => { let daysOptionLabel; let hoursOptionLabel; let minutesOptionLabel; @@ -192,15 +186,17 @@ export const MinAgeInput: React.FunctionComponent = ({ ); } + // check that these strings are valid properties + const selectedMinimumAgeProperty = propertyof('selectedMinimumAge'); + const selectedMinimumAgeUnitsProperty = propertyof('selectedMinimumAgeUnits'); return ( = ({ } > { - setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE, e.target.value); + setPhaseData(selectedMinimumAgeProperty, e.target.value); }} min={0} /> @@ -227,8 +223,8 @@ export const MinAgeInput: React.FunctionComponent = ({ setPhaseData(PHASE_ROLLOVER_MINIMUM_AGE_UNITS, e.target.value)} + value={phaseData.selectedMinimumAgeUnits} + onChange={(e) => setPhaseData(selectedMinimumAgeUnitsProperty, e.target.value)} options={[ { value: 'd', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 576483a5ab9c2..0ce2c0d7ea566 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -16,20 +16,12 @@ import { EuiButton, } from '@elastic/eui'; -import { PHASE_NODE_ATTRS } from '../../../constants'; import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { useLoadNodes } from '../../../services/api'; import { NodeAttrsDetails } from './node_attrs_details'; - -interface Props { - phase: string; - errors: Record; - // TODO add types for phaseData and setPhaseData after policy is typed - phaseData: any; - setPhaseData: (dataKey: string, value: any) => void; - isShowingErrors: boolean; -} +import { ColdPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; const learnMoreLink = ( @@ -46,13 +38,20 @@ const learnMoreLink = ( ); -export const NodeAllocation: React.FunctionComponent = ({ +interface Props { + phase: keyof Phases & string; + errors?: PhaseValidationErrors; + phaseData: T; + setPhaseData: (dataKey: keyof T & string, value: string) => void; + isShowingErrors: boolean; +} +export const NodeAllocation = ({ phase, setPhaseData, errors, phaseData, isShowingErrors, -}) => { +}: React.PropsWithChildren>) => { const { isLoading, data: nodes, error, sendRequest } = useLoadNodes(); const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( @@ -140,33 +139,35 @@ export const NodeAllocation: React.FunctionComponent = ({ ); } + // check that this string is a valid property + const nodeAttrsProperty = propertyof('selectedNodeAttrs'); + return ( { - setPhaseData(PHASE_NODE_ATTRS, e.target.value); + setPhaseData(nodeAttrsProperty, e.target.value); }} /> - {!!phaseData[PHASE_NODE_ATTRS] ? ( + {!!phaseData.selectedNodeAttrs ? ( setSelectedNodeAttrsForDetails(phaseData[PHASE_NODE_ATTRS])} + onClick={() => setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)} > void; - // TODO add types for lifecycle after policy is typed - lifecycle: any; + policy: Policy; policyName: string; } -export const PolicyJsonFlyout: React.FunctionComponent = ({ - close, - lifecycle, - policyName, -}) => { - // @ts-ignore until store is typed - const getEsJson = ({ phases }) => { +export const PolicyJsonFlyout: React.FunctionComponent = ({ close, policy, policyName }) => { + const getEsJson = ({ phases }: Policy) => { return JSON.stringify( { policy: { @@ -45,7 +40,7 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({ }; const endpoint = `PUT _ilm/policy/${policyName || ''}`; - const request = `${endpoint}\n${getEsJson(lifecycle)}`; + const request = `${endpoint}\n${getEsJson(policy)}`; return ( 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 0034de85fce17..1da7508049f24 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 @@ -7,27 +7,27 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; -import { PHASE_INDEX_PRIORITY } from '../../../constants'; - import { LearnMoreLink } from './'; import { OptionalLabel } from './'; import { ErrableFormRow } from './'; +import { ColdPhase, HotPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -interface Props { - errors: Record; - // TODO add types for phaseData and setPhaseData after policy is typed - phase: string; - phaseData: any; - setPhaseData: (dataKey: string, value: any) => void; +interface Props { + errors?: PhaseValidationErrors; + phase: keyof Phases & string; + phaseData: T; + setPhaseData: (dataKey: keyof T & string, value: any) => void; isShowingErrors: boolean; } -export const SetPriorityInput: React.FunctionComponent = ({ +export const SetPriorityInput = ({ errors, phaseData, phase, setPhaseData, isShowingErrors, -}) => { +}: React.PropsWithChildren>) => { + const phaseIndexPriorityProperty = propertyof('phaseIndexPriority'); return ( = ({ fullWidth > = ({ } - errorKey={PHASE_INDEX_PRIORITY} isShowingErrors={isShowingErrors} - errors={errors} + errors={errors?.phaseIndexPriority} > { - setPhaseData(PHASE_INDEX_PRIORITY, e.target.value); + setPhaseData(phaseIndexPriorityProperty, e.target.value); }} min={0} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js deleted file mode 100644 index e7f20a66d09f0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js +++ /dev/null @@ -1,58 +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 { connect } from 'react-redux'; - -import { - getSaveAsNewPolicy, - getSelectedPolicy, - validateLifecycle, - getLifecycle, - getPolicies, - isPolicyListLoaded, - getIsNewPolicy, - getSelectedOriginalPolicyName, - getPhases, -} from '../../store/selectors'; - -import { - setSelectedPolicy, - setSelectedPolicyName, - setSaveAsNewPolicy, - saveLifecyclePolicy, - fetchPolicies, - setPhaseData, -} from '../../store/actions'; - -import { findFirstError } from '../../services/find_errors'; -import { EditPolicy as PresentationComponent } from './edit_policy'; - -export const EditPolicy = connect( - (state) => { - const errors = validateLifecycle(state); - const firstError = findFirstError(errors); - return { - firstError, - errors, - selectedPolicy: getSelectedPolicy(state), - saveAsNewPolicy: getSaveAsNewPolicy(state), - lifecycle: getLifecycle(state), - policies: getPolicies(state), - isPolicyListLoaded: isPolicyListLoaded(state), - isNewPolicy: getIsNewPolicy(state), - originalPolicyName: getSelectedOriginalPolicyName(state), - phases: getPhases(state), - }; - }, - { - setSelectedPolicy, - setSelectedPolicyName, - setSaveAsNewPolicy, - saveLifecyclePolicy, - fetchPolicies, - setPhaseData, - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx new file mode 100644 index 0000000000000..359134e015f7f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -0,0 +1,83 @@ +/* + * 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 from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useLoadPoliciesList } from '../../services/api'; + +import { EditPolicy as PresentationComponent } from './edit_policy'; + +interface RouterProps { + policyName: string; +} + +interface Props { + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; +} + +export const EditPolicy: React.FunctionComponent> = ({ + match: { + params: { policyName }, + }, + getUrlForApp, + history, +}) => { + const { error, isLoading, data: policies, sendRequest } = useLoadPoliciesList(false); + if (isLoading) { + return ( + } + body={ + + } + /> + ); + } + if (error || !policies) { + const { statusCode, message } = error ? error : { statusCode: '', message: '' }; + return ( + + } + color="danger" + > + + {message} ({statusCode}) + + + + + + ); + } + + return ( + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js deleted file mode 100644 index a29ecd07c5e45..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js +++ /dev/null @@ -1,390 +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, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { - EuiPage, - EuiPageBody, - EuiFieldText, - EuiPageContent, - EuiFormRow, - EuiTitle, - EuiText, - EuiSpacer, - EuiSwitch, - EuiHorizontalRule, - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiDescribedFormGroup, -} from '@elastic/eui'; - -import { - PHASE_HOT, - PHASE_COLD, - PHASE_DELETE, - PHASE_WARM, - STRUCTURE_POLICY_NAME, - WARM_PHASE_ON_ROLLOVER, - PHASE_ROLLOVER_ENABLED, -} from '../../constants'; - -import { toasts } from '../../services/notification'; -import { findFirstError } from '../../services/find_errors'; -import { LearnMoreLink, PolicyJsonFlyout, ErrableFormRow } from './components'; - -import { HotPhase, WarmPhase, ColdPhase, DeletePhase } from './phases'; - -export class EditPolicy extends Component { - static propTypes = { - selectedPolicy: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, - }; - - constructor(props) { - super(props); - this.state = { - isShowingErrors: false, - isShowingPolicyJsonFlyout: false, - }; - } - - selectPolicy = (policyName) => { - const { setSelectedPolicy, policies } = this.props; - - const selectedPolicy = policies.find((policy) => { - return policy.name === policyName; - }); - - if (selectedPolicy) { - setSelectedPolicy(selectedPolicy); - } - }; - - componentDidMount() { - window.scrollTo(0, 0); - - const { - isPolicyListLoaded, - fetchPolicies, - match: { params: { policyName } } = { params: {} }, - } = this.props; - - if (policyName) { - const decodedPolicyName = decodeURIComponent(policyName); - if (isPolicyListLoaded) { - this.selectPolicy(decodedPolicyName); - } else { - fetchPolicies(true, () => { - this.selectPolicy(decodedPolicyName); - }); - } - } else { - this.props.setSelectedPolicy(null); - } - } - - backToPolicyList = () => { - this.props.setSelectedPolicy(null); - this.props.history.push('/policies'); - }; - - submit = async () => { - this.setState({ isShowingErrors: true }); - const { saveLifecyclePolicy, lifecycle, saveAsNewPolicy, firstError } = this.props; - if (firstError) { - toasts.addDanger( - i18n.translate('xpack.indexLifecycleMgmt.editPolicy.formErrorsMessage', { - defaultMessage: 'Please fix the errors on this page.', - }) - ); - const errorRowId = `${firstError.replace('.', '-')}-row`; - const element = document.getElementById(errorRowId); - if (element) { - element.scrollIntoView({ block: 'center', inline: 'nearest' }); - } - } else { - const success = await saveLifecyclePolicy(lifecycle, saveAsNewPolicy); - if (success) { - this.backToPolicyList(); - } - } - }; - - togglePolicyJsonFlyout = () => { - this.setState(({ isShowingPolicyJsonFlyout }) => ({ - isShowingPolicyJsonFlyout: !isShowingPolicyJsonFlyout, - })); - }; - - render() { - const { - selectedPolicy, - errors, - setSaveAsNewPolicy, - saveAsNewPolicy, - setSelectedPolicyName, - isNewPolicy, - lifecycle, - originalPolicyName, - phases, - setPhaseData, - } = this.props; - const selectedPolicyName = selectedPolicy.name; - const { isShowingErrors, isShowingPolicyJsonFlyout } = this.state; - - return ( - - - - - - {isNewPolicy - ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { - defaultMessage: 'Create an index lifecycle policy', - }) - : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { - defaultMessage: 'Edit index lifecycle policy {originalPolicyName}', - values: { originalPolicyName }, - })} - - - - - - - - {' '} - - } - /> - - - - - - - {isNewPolicy ? null : ( - - - - - - - - .{' '} - - - - - - - - { - await setSaveAsNewPolicy(e.target.checked); - }} - label={ - - - - } - /> - - - )} - - {saveAsNewPolicy || isNewPolicy ? ( - - - - - - } - titleSize="s" - fullWidth - > - - } - > - { - await setSelectedPolicyName(e.target.value); - }} - /> - - - ) : null} - - - - - setPhaseData(PHASE_HOT, key, value)} - phaseData={phases[PHASE_HOT]} - setWarmPhaseOnRollover={(value) => - setPhaseData(PHASE_WARM, WARM_PHASE_ON_ROLLOVER, value) - } - /> - - - - setPhaseData(PHASE_WARM, key, value)} - phaseData={phases[PHASE_WARM]} - hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} - /> - - - - setPhaseData(PHASE_COLD, key, value)} - phaseData={phases[PHASE_COLD]} - hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} - /> - - - - setPhaseData(PHASE_DELETE, key, value)} - phaseData={phases[PHASE_DELETE]} - hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} - /> - - - - - - - - - {saveAsNewPolicy ? ( - - ) : ( - - )} - - - - - - - - - - - - - - {isShowingPolicyJsonFlyout ? ( - - ) : ( - - )} - - - - - {this.state.isShowingPolicyJsonFlyout ? ( - this.setState({ isShowingPolicyJsonFlyout: false })} - /> - ) : null} - - - - - ); - } -} 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 new file mode 100644 index 0000000000000..6cffde577b35e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -0,0 +1,383 @@ +/* + * 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, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiButtonEmpty, + EuiDescribedFormGroup, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { toasts } from '../../services/notification'; + +import { Policy, PolicyFromES } from '../../services/policies/types'; +import { + validatePolicy, + ValidationErrors, + findFirstError, +} from '../../services/policies/policy_validation'; +import { savePolicy } from '../../services/policies/policy_save'; +import { + deserializePolicy, + getPolicyByName, + initializeNewPolicy, +} from '../../services/policies/policy_serialization'; + +import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; +import { ColdPhase, DeletePhase, HotPhase, WarmPhase } from './phases'; + +interface Props { + policies: PolicyFromES[]; + policyName: string; + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; + history: any; +} +export const EditPolicy: React.FunctionComponent = ({ + policies, + policyName, + history, + getUrlForApp, +}) => { + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + const [isShowingErrors, setIsShowingErrors] = useState(false); + const [errors, setErrors] = useState(); + const [isShowingPolicyJsonFlyout, setIsShowingPolicyJsonFlyout] = useState(false); + + const existingPolicy = getPolicyByName(policies, policyName); + + const [policy, setPolicy] = useState( + existingPolicy ? deserializePolicy(existingPolicy) : initializeNewPolicy(policyName) + ); + + const isNewPolicy: boolean = !Boolean(existingPolicy); + const [saveAsNew, setSaveAsNew] = useState(isNewPolicy); + const originalPolicyName: string = existingPolicy ? existingPolicy.name : ''; + + const backToPolicyList = () => { + history.push('/policies'); + }; + + const submit = async () => { + setIsShowingErrors(true); + const [isValid, validationErrors] = validatePolicy( + saveAsNew, + policy, + policies, + originalPolicyName + ); + setErrors(validationErrors); + + 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' }); + } + } else { + const success = await savePolicy(policy, isNewPolicy || saveAsNew, existingPolicy); + if (success) { + backToPolicyList(); + } + } + }; + + const togglePolicyJsonFlyout = () => { + setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout); + }; + + const setPhaseData = (phase: 'hot' | 'warm' | 'cold' | 'delete', key: string, value: any) => { + setPolicy({ + ...policy, + phases: { + ...policy.phases, + [phase]: { ...policy.phases[phase], [key]: value }, + }, + }); + }; + + const setWarmPhaseOnRollover = (value: boolean) => { + setPolicy({ + ...policy, + phases: { + ...policy.phases, + hot: { + ...policy.phases.hot, + rolloverEnabled: value, + }, + warm: { + ...policy.phases.warm, + warmPhaseOnRollover: value, + }, + }, + }); + }; + + return ( + + + + + + {isNewPolicy + ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { + defaultMessage: 'Create an index lifecycle policy', + }) + : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { + defaultMessage: 'Edit index lifecycle policy {originalPolicyName}', + values: { originalPolicyName }, + })} + + + + + + + + {' '} + + } + /> + + + + + + {isNewPolicy ? null : ( + + + + + + + .{' '} + + + + + + + { + setSaveAsNew(e.target.checked); + }} + label={ + + + + } + /> + + + )} + + {saveAsNew || isNewPolicy ? ( + + + + + + } + titleSize="s" + fullWidth + > + + } + > + { + setPolicy({ ...policy, name: e.target.value }); + }} + /> + + + ) : null} + + + + 0} + setPhaseData={(key, value) => setPhaseData('hot', key, value)} + phaseData={policy.phases.hot} + setWarmPhaseOnRollover={setWarmPhaseOnRollover} + /> + + + + 0} + setPhaseData={(key, value) => setPhaseData('warm', key, value)} + phaseData={policy.phases.warm} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + + 0} + setPhaseData={(key, value) => setPhaseData('cold', key, value)} + phaseData={policy.phases.cold} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + + 0} + getUrlForApp={getUrlForApp} + setPhaseData={(key, value) => setPhaseData('delete', key, value)} + phaseData={policy.phases.delete} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + + + + + + + {saveAsNew ? ( + + ) : ( + + )} + + + + + + + + + + + + + + {isShowingPolicyJsonFlyout ? ( + + ) : ( + + )} + + + + + {isShowingPolicyJsonFlyout ? ( + setIsShowingPolicyJsonFlyout(false)} + /> + ) : null} + + + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts deleted file mode 100644 index 5f15d929a4916..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.d.ts +++ /dev/null @@ -1,7 +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. - */ - -export declare const EditPolicy: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/index.ts 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/phases/cold_phase.tsx index babbbf7638ebe..fb32752fe24ea 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/phases/cold_phase.tsx @@ -18,12 +18,9 @@ import { EuiTextColor, } from '@elastic/eui'; -import { - PHASE_COLD, - PHASE_ENABLED, - PHASE_REPLICA_COUNT, - PHASE_FREEZE_ENABLED, -} from '../../../constants'; +import { ColdPhase as ColdPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + import { LearnMoreLink, ActiveBadge, @@ -35,14 +32,21 @@ import { SetPriorityInput, } from '../components'; +const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', +}); + +const coldProperty = propertyof('cold'); +const phaseProperty = (propertyName: keyof ColdPhaseInterface) => + propertyof(propertyName); + interface Props { - setPhaseData: (key: string, value: any) => void; - phaseData: any; + setPhaseData: (key: keyof ColdPhaseInterface & string, value: string | boolean) => void; + phaseData: ColdPhaseInterface; isShowingErrors: boolean; - errors: Record; + errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } - export class ColdPhase extends PureComponent { render() { const { @@ -53,10 +57,6 @@ export class ColdPhase extends PureComponent { hotPhaseRolloverEnabled, } = this.props; - const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { - defaultMessage: 'Freeze index', - }); - return ( { defaultMessage="Cold phase" /> {' '} - {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null} + {phaseData.phaseEnabled && !isShowingErrors ? : null} } @@ -91,10 +91,10 @@ export class ColdPhase extends PureComponent { defaultMessage="Activate cold phase" /> } - id={`${PHASE_COLD}-${PHASE_ENABLED}`} - checked={phaseData[PHASE_ENABLED]} + id={`${coldProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} onChange={(e) => { - setPhaseData(PHASE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); }} aria-controls="coldPhaseContent" /> @@ -103,20 +103,20 @@ export class ColdPhase extends PureComponent { fullWidth > - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( - errors={errors} phaseData={phaseData} - phase={PHASE_COLD} + phase={coldProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} rolloverEnabled={hotPhaseRolloverEnabled} /> - + phase={coldProperty} setPhaseData={setPhaseData} errors={errors} phaseData={phaseData} @@ -126,7 +126,7 @@ export class ColdPhase extends PureComponent { { } - errorKey={PHASE_REPLICA_COUNT} isShowingErrors={isShowingErrors} - errors={errors} + errors={errors?.freezeEnabled} helpText={i18n.translate( 'xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText', { @@ -147,10 +146,10 @@ export class ColdPhase extends PureComponent { )} > { - setPhaseData(PHASE_REPLICA_COUNT, e.target.value); + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); }} min={0} /> @@ -163,7 +162,7 @@ export class ColdPhase extends PureComponent { )} - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( { > { - setPhaseData(PHASE_FREEZE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); }} label={freezeLabel} aria-label={freezeLabel} /> - errors={errors} phaseData={phaseData} - phase={PHASE_COLD} + phase={coldProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} /> 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 index 0143cc4af24e3..d3c73090f25f2 100644 --- 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 @@ -8,7 +8,9 @@ import React, { PureComponent, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; -import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../constants'; +import { DeletePhase as DeletePhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + import { ActiveBadge, LearnMoreLink, @@ -18,11 +20,15 @@ import { SnapshotPolicies, } from '../components'; +const deleteProperty = propertyof('delete'); +const phaseProperty = (propertyName: keyof DeletePhaseInterface) => + propertyof(propertyName); + interface Props { - setPhaseData: (key: string, value: any) => void; - phaseData: any; + setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void; + phaseData: DeletePhaseInterface; isShowingErrors: boolean; - errors: Record; + errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; getUrlForApp: ( appId: string, @@ -55,7 +61,7 @@ export class DeletePhase extends PureComponent { defaultMessage="Delete phase" /> {' '} - {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null} + {phaseData.phaseEnabled && !isShowingErrors ? : null} } @@ -76,10 +82,10 @@ export class DeletePhase extends PureComponent { defaultMessage="Activate delete phase" /> } - id={`${PHASE_DELETE}-${PHASE_ENABLED}`} - checked={phaseData[PHASE_ENABLED]} + id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} onChange={(e) => { - setPhaseData(PHASE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); }} aria-controls="deletePhaseContent" /> @@ -87,11 +93,11 @@ export class DeletePhase extends PureComponent { } fullWidth > - {phaseData[PHASE_ENABLED] ? ( - errors={errors} phaseData={phaseData} - phase={PHASE_DELETE} + phase={deleteProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} rolloverEnabled={hotPhaseRolloverEnabled} @@ -100,7 +106,7 @@ export class DeletePhase extends PureComponent { )} - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( @@ -135,8 +141,8 @@ export class DeletePhase extends PureComponent { } > setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, value)} + value={phaseData.waitForSnapshotPolicy} + onChange={(value) => setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)} getUrlForApp={getUrlForApp} /> 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 index dbd48f3a85634..22f0114d16afe 100644 --- 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 @@ -7,7 +7,6 @@ import React, { Fragment, PureComponent } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; - import { EuiFlexGroup, EuiFlexItem, @@ -19,15 +18,9 @@ import { EuiDescribedFormGroup, } from '@elastic/eui'; -import { - PHASE_HOT, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_AGE_UNITS, - PHASE_ROLLOVER_MAX_DOCUMENTS, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, - PHASE_ROLLOVER_ENABLED, -} from '../../../constants'; +import { HotPhase as HotPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + import { LearnMoreLink, ActiveBadge, @@ -36,11 +29,98 @@ import { SetPriorityInput, } 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 = propertyof('hot'); +const phaseProperty = (propertyName: keyof HotPhaseInterface) => + propertyof(propertyName); + interface Props { - errors: Record; + errors?: PhaseValidationErrors; isShowingErrors: boolean; - phaseData: any; - setPhaseData: (key: string, value: any) => void; + phaseData: HotPhaseInterface; + setPhaseData: (key: keyof HotPhaseInterface & string, value: string | boolean) => void; setWarmPhaseOnRollover: (value: boolean) => void; } @@ -104,39 +184,36 @@ export class HotPhase extends PureComponent { > { - const { checked } = e.target; - setPhaseData(PHASE_ROLLOVER_ENABLED, checked); - setWarmPhaseOnRollover(checked); + setWarmPhaseOnRollover(e.target.checked); }} label={i18n.translate('xpack.indexLifecycleMgmt.hotPhase.enableRolloverLabel', { defaultMessage: 'Enable rollover', })} /> - {phaseData[PHASE_ROLLOVER_ENABLED] ? ( + {phaseData.rolloverEnabled ? ( { - setPhaseData(PHASE_ROLLOVER_MAX_SIZE_STORED, e.target.value); + setPhaseData(phaseProperty('selectedMaxSizeStored'), e.target.value); }} min={1} /> @@ -144,11 +221,10 @@ export class HotPhase extends PureComponent { { defaultMessage: 'Maximum index size units', } )} - value={phaseData[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]} + value={phaseData.selectedMaxSizeStoredUnits} onChange={(e) => { - setPhaseData(PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, e.target.value); + setPhaseData(phaseProperty('selectedMaxSizeStoredUnits'), e.target.value); }} - options={[ - { - 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', - }), - }, - ]} + options={maxSizeStoredUnits} /> @@ -207,22 +246,21 @@ export class HotPhase extends PureComponent { { - setPhaseData(PHASE_ROLLOVER_MAX_DOCUMENTS, e.target.value); + setPhaseData(phaseProperty('selectedMaxDocuments'), e.target.value); }} min={1} /> @@ -233,19 +271,18 @@ export class HotPhase extends PureComponent { { - setPhaseData(PHASE_ROLLOVER_MAX_AGE, e.target.value); + setPhaseData(phaseProperty('selectedMaxAge'), e.target.value); }} min={1} /> @@ -253,11 +290,10 @@ export class HotPhase extends PureComponent { { defaultMessage: 'Maximum age units', } )} - value={phaseData[PHASE_ROLLOVER_MAX_AGE_UNITS]} + value={phaseData.selectedMaxAgeUnits} onChange={(e) => { - setPhaseData(PHASE_ROLLOVER_MAX_AGE_UNITS, e.target.value); + setPhaseData(phaseProperty('selectedMaxAgeUnits'), e.target.value); }} - options={[ - { - 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', - } - ), - }, - ]} + options={maxAgeUnits} /> @@ -330,10 +314,10 @@ export class HotPhase extends PureComponent { ) : null} - errors={errors} phaseData={phaseData} - phase={PHASE_HOT} + phase={hotProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} /> 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/phases/warm_phase.tsx index 6ed81bf8f45d5..f7b8c60a5c71f 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/phases/warm_phase.tsx @@ -18,16 +18,6 @@ import { EuiDescribedFormGroup, } from '@elastic/eui'; -import { - PHASE_WARM, - PHASE_ENABLED, - WARM_PHASE_ON_ROLLOVER, - PHASE_FORCE_MERGE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_SHRINK_ENABLED, -} from '../../../constants'; import { LearnMoreLink, ActiveBadge, @@ -39,11 +29,33 @@ import { MinAgeInput, } from '../components'; +import { Phases, WarmPhase as WarmPhaseInterface } from '../../../services/policies/types'; +import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; + +const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { + defaultMessage: 'Shrink index', +}); + +const moveToWarmPhaseOnRolloverLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', + { + defaultMessage: 'Move to warm phase on rollover', + } +); + +const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.forceMergeDataLabel', { + defaultMessage: 'Force merge data', +}); + +const warmProperty = propertyof('warm'); +const phaseProperty = (propertyName: keyof WarmPhaseInterface) => + propertyof(propertyName); + interface Props { - setPhaseData: (key: string, value: any) => void; - phaseData: any; + setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void; + phaseData: WarmPhaseInterface; isShowingErrors: boolean; - errors: Record; + errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } export class WarmPhase extends PureComponent { @@ -56,24 +68,6 @@ export class WarmPhase extends PureComponent { hotPhaseRolloverEnabled, } = this.props; - const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { - defaultMessage: 'Shrink index', - }); - - const moveToWarmPhaseOnRolloverLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', - { - defaultMessage: 'Move to warm phase on rollover', - } - ); - - const forcemergeLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.forceMergeDataLabel', - { - defaultMessage: 'Force merge data', - } - ); - return ( { defaultMessage="Warm phase" /> {' '} - {phaseData[PHASE_ENABLED] && !isShowingErrors ? : null} + {phaseData.phaseEnabled && !isShowingErrors ? : null} } @@ -108,10 +102,10 @@ export class WarmPhase extends PureComponent { defaultMessage="Activate warm phase" /> } - id={`${PHASE_WARM}-${PHASE_ENABLED}`} - checked={phaseData[PHASE_ENABLED]} + id={`${warmProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} onChange={(e) => { - setPhaseData(PHASE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); }} aria-controls="warmPhaseContent" /> @@ -120,28 +114,28 @@ export class WarmPhase extends PureComponent { fullWidth > - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( {hotPhaseRolloverEnabled ? ( - + { - setPhaseData(WARM_PHASE_ON_ROLLOVER, e.target.checked); + setPhaseData(phaseProperty('warmPhaseOnRollover'), e.target.checked); }} /> ) : null} - {!phaseData[WARM_PHASE_ON_ROLLOVER] ? ( + {!phaseData.warmPhaseOnRollover ? ( - errors={errors} phaseData={phaseData} - phase={PHASE_WARM} + phase={warmProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} rolloverEnabled={hotPhaseRolloverEnabled} @@ -151,8 +145,8 @@ export class WarmPhase extends PureComponent { - + phase={warmProperty} setPhaseData={setPhaseData} errors={errors} phaseData={phaseData} @@ -162,7 +156,7 @@ export class WarmPhase extends PureComponent { { } - errorKey={PHASE_REPLICA_COUNT} isShowingErrors={isShowingErrors} - errors={errors} + errors={errors?.selectedReplicaCount} helpText={i18n.translate( 'xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText', { @@ -183,10 +176,10 @@ export class WarmPhase extends PureComponent { )} > { - setPhaseData(PHASE_REPLICA_COUNT, e.target.value); + setPhaseData('selectedReplicaCount', e.target.value); }} min={0} /> @@ -199,7 +192,7 @@ export class WarmPhase extends PureComponent { ) : null} - {phaseData[PHASE_ENABLED] ? ( + {phaseData.phaseEnabled ? ( { { - setPhaseData(PHASE_SHRINK_ENABLED, e.target.checked); + setPhaseData(phaseProperty('shrinkEnabled'), e.target.checked); }} label={shrinkLabel} aria-label={shrinkLabel} @@ -235,28 +228,30 @@ export class WarmPhase extends PureComponent { /> - {phaseData[PHASE_SHRINK_ENABLED] ? ( + {phaseData.shrinkEnabled ? ( { - setPhaseData(PHASE_PRIMARY_SHARD_COUNT, e.target.value); + setPhaseData( + phaseProperty('selectedPrimaryShardCount'), + e.target.value + ); }} min={1} /> @@ -294,33 +289,32 @@ export class WarmPhase extends PureComponent { data-test-subj="forceMergeSwitch" label={forcemergeLabel} aria-label={forcemergeLabel} - checked={phaseData[PHASE_FORCE_MERGE_ENABLED]} + checked={phaseData.forceMergeEnabled} onChange={(e) => { - setPhaseData(PHASE_FORCE_MERGE_ENABLED, e.target.checked); + setPhaseData(phaseProperty('forceMergeEnabled'), e.target.checked); }} aria-controls="forcemergeContent" /> - {phaseData[PHASE_FORCE_MERGE_ENABLED] ? ( + {phaseData.forceMergeEnabled ? ( { - setPhaseData(PHASE_FORCE_MERGE_SEGMENTS, e.target.value); + setPhaseData(phaseProperty('selectedForceMergeSegments'), e.target.value); }} min={1} /> @@ -328,10 +322,10 @@ export class WarmPhase extends PureComponent { ) : null} - errors={errors} phaseData={phaseData} - phase={PHASE_WARM} + phase={warmProperty} isShowingErrors={isShowingErrors} setPhaseData={setPhaseData} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js index 500ab44d96694..ec1cdb987f4b3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js @@ -38,7 +38,7 @@ import { import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; import { getIndexListUri } from '../../../../../../../index_management/public'; -import { UIM_EDIT_CLICK } from '../../../../constants'; +import { UIM_EDIT_CLICK } from '../../../../constants/ui_metric'; import { getPolicyPath } from '../../../../services/navigation'; import { flattenPanelTree } from '../../../../services/flatten_panel_tree'; import { trackUiMetric } from '../../../../services/ui_metric'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index 61de37bbfad11..b80e9e70c54fa 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -12,10 +12,11 @@ import { UIM_POLICY_ATTACH_INDEX_TEMPLATE, UIM_POLICY_DETACH_INDEX, UIM_INDEX_RETRY_STEP, -} from '../constants'; +} from '../constants/ui_metric'; import { trackUiMetric } from './ui_metric'; import { sendGet, sendPost, sendDelete, useRequest } from './http'; +import { PolicyFromES, SerializedPolicy } from './policies/types'; interface GenericObject { [key: string]: any; @@ -44,7 +45,15 @@ export async function loadPolicies(withIndices: boolean) { return await sendGet('policies', { withIndices }); } -export async function savePolicy(policy: GenericObject) { +export const useLoadPoliciesList = (withIndices: boolean) => { + return useRequest({ + path: `policies`, + method: 'get', + query: { withIndices }, + }); +}; + +export async function savePolicy(policy: SerializedPolicy) { return await sendPost(`policies`, policy); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js b/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js deleted file mode 100644 index 12b53ad1eaf52..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/find_errors.js +++ /dev/null @@ -1,24 +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. - */ - -export const findFirstError = (object, topLevel = true) => { - let firstError; - const keys = topLevel ? ['policyName', 'hot', 'warm', 'cold', 'delete'] : Object.keys(object); - for (const key of keys) { - const value = object[key]; - if (Array.isArray(value) && value.length > 0) { - firstError = key; - break; - } else if (value) { - firstError = findFirstError(value, false); - if (firstError) { - firstError = `${key}.${firstError}`; - break; - } - } - } - return firstError; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts new file mode 100644 index 0000000000000..d9ed7a0bf51eb --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -0,0 +1,159 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { serializedPhaseInitialization } from '../../constants'; +import { AllocateAction, ColdPhase, SerializedColdPhase } from './types'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; +import { + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, +} from './policy_validation'; + +const coldPhaseInitialization: ColdPhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + freezeEnabled: false, + phaseIndexPriority: '', +}; + +export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhase => { + const phase = { ...coldPhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.freeze) { + phase.freezeEnabled = true; + } + + if (actions.set_priority) { + phase.phaseIndexPriority = actions.set_priority.priority + ? actions.set_priority.priority.toString() + : ''; + } + } + + return phase; +}; + +export const coldPhaseToES = ( + phase: ColdPhase, + originalPhase: SerializedColdPhase | undefined +): SerializedColdPhase => { + if (!originalPhase) { + originalPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.freezeEnabled) { + esPhase.actions.freeze = {}; + } else { + delete esPhase.actions.freeze; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateColdPhase = (phase: ColdPhase): 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]; + } + } + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts new file mode 100644 index 0000000000000..70e7c21da8cb6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts @@ -0,0 +1,88 @@ +/* + * 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 { serializedPhaseInitialization } from '../../constants'; +import { DeletePhase, SerializedDeletePhase } from './types'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; +import { + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, +} from './policy_validation'; + +const deletePhaseInitialization: DeletePhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + waitForSnapshotPolicy: '', +}; + +export const deletePhaseFromES = (phaseSerialized?: SerializedDeletePhase): DeletePhase => { + const phase = { ...deletePhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + + if (actions.wait_for_snapshot) { + phase.waitForSnapshotPolicy = actions.wait_for_snapshot.policy; + } + } + + return phase; +}; + +export const deletePhaseToES = ( + phase: DeletePhase, + originalEsPhase?: SerializedDeletePhase +): SerializedDeletePhase => { + if (!originalEsPhase) { + originalEsPhase = { ...serializedPhaseInitialization }; + } + const esPhase = { ...originalEsPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.waitForSnapshotPolicy) { + esPhase.actions.wait_for_snapshot = { + policy: phase.waitForSnapshotPolicy, + }; + } else { + delete esPhase.actions.wait_for_snapshot; + } + + return esPhase; +}; + +export const validateDeletePhase = (phase: DeletePhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; 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 new file mode 100644 index 0000000000000..34ac8f3e270e6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts @@ -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 { serializedPhaseInitialization } from '../../constants'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; +import { HotPhase, SerializedHotPhase } from './types'; +import { + maximumAgeRequiredMessage, + maximumDocumentsRequiredMessage, + maximumSizeRequiredMessage, + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, + positiveNumbersAboveZeroErrorMessage, +} from './policy_validation'; + +const hotPhaseInitialization: HotPhase = { + phaseEnabled: false, + rolloverEnabled: false, + selectedMaxAge: '', + selectedMaxAgeUnits: 'd', + selectedMaxSizeStored: '', + selectedMaxSizeStoredUnits: 'gb', + 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.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); + } + } else { + delete esPhase.actions.rollover; + } + + 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]; + } + } + + return { + ...phaseErrors, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts similarity index 58% rename from x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts index 0bb6543482bd6..12df071544952 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/lifecycle.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts @@ -5,28 +5,36 @@ */ import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; -import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants'; -import { showApiError } from '../../services/api_errors'; -import { toasts } from '../../services/notification'; -import { savePolicy as savePolicyApi } from '../../services/api'; -import { trackUiMetric, getUiMetricsForPhases } from '../../services/ui_metric'; +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/ui_metric'; +import { toasts } from '../notification'; +import { Policy, PolicyFromES } from './types'; +import { serializePolicy } from './policy_serialization'; -export const saveLifecyclePolicy = (lifecycle, isNew) => async () => { +export const savePolicy = async ( + policy: Policy, + isNew: boolean, + originalEsPolicy?: PolicyFromES +): Promise => { + const serializedPolicy = serializePolicy(policy, originalEsPolicy?.policy); try { - await savePolicyApi(lifecycle); + await savePolicyApi(serializedPolicy); } catch (err) { const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage', { defaultMessage: 'Error saving lifecycle policy {lifecycleName}', - values: { lifecycleName: lifecycle.name }, + values: { lifecycleName: policy.name }, }); showApiError(err, title); return false; } - const uiMetrics = getUiMetricsForPhases(lifecycle.phases); + const uiMetrics = getUiMetricsForPhases(serializedPolicy.phases); uiMetrics.push(isNew ? UIM_POLICY_CREATE : UIM_POLICY_UPDATE); - trackUiMetric('count', uiMetrics); + trackUiMetric(METRIC_TYPE.COUNT, uiMetrics); const message = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage', { defaultMessage: '{verb} lifecycle policy "{lifecycleName}"', @@ -38,7 +46,7 @@ export const saveLifecyclePolicy = (lifecycle, isNew) => async () => { : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.updatedMessage', { defaultMessage: 'Updated', }), - lifecycleName: lifecycle.name, + lifecycleName: policy.name, }, }); toasts.addSuccess(message); 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 new file mode 100644 index 0000000000000..3953521df1817 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -0,0 +1,104 @@ +/* + * 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 { + defaultNewColdPhase, + defaultNewDeletePhase, + defaultNewHotPhase, + defaultNewWarmPhase, + serializedPhaseInitialization, +} from '../../constants'; + +import { Policy, PolicyFromES, SerializedPolicy } from './types'; + +import { hotPhaseFromES, hotPhaseToES } from './hot_phase'; +import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; +import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; +import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; + +export const splitSizeAndUnits = (field: string): { size: string; units: string } => { + let size = ''; + let units = ''; + + const result = /(\d+)(\w+)/.exec(field); + if (result) { + size = result[1]; + units = result[2]; + } + + return { + size, + units, + }; +}; + +export const isNumber = (value: any): boolean => value !== '' && value !== null && isFinite(value); + +export const getPolicyByName = ( + policies: PolicyFromES[] | null | undefined, + policyName: string = '' +): PolicyFromES | undefined => { + if (policies && policies.length > 0) { + return policies.find((policy: PolicyFromES) => policy.name === policyName); + } +}; + +export const initializeNewPolicy = (newPolicyName: string = ''): Policy => { + return { + name: newPolicyName, + phases: { + hot: { ...defaultNewHotPhase }, + warm: { ...defaultNewWarmPhase }, + cold: { ...defaultNewColdPhase }, + delete: { ...defaultNewDeletePhase }, + }, + }; +}; + +export const deserializePolicy = (policy: PolicyFromES): Policy => { + const { + name, + policy: { phases }, + } = policy; + + return { + name, + phases: { + hot: hotPhaseFromES(phases.hot), + warm: warmPhaseFromES(phases.warm), + cold: coldPhaseFromES(phases.cold), + delete: deletePhaseFromES(phases.delete), + }, + }; +}; + +export const serializePolicy = ( + policy: Policy, + originalEsPolicy: SerializedPolicy = { + name: policy.name, + phases: { hot: { ...serializedPhaseInitialization } }, + } +): SerializedPolicy => { + const serializedPolicy = { + name: policy.name, + phases: { hot: hotPhaseToES(policy.phases.hot, originalEsPolicy.phases.hot) }, + } as SerializedPolicy; + if (policy.phases.warm.phaseEnabled) { + serializedPolicy.phases.warm = warmPhaseToES(policy.phases.warm, originalEsPolicy.phases.warm); + } + + if (policy.phases.cold.phaseEnabled) { + serializedPolicy.phases.cold = coldPhaseToES(policy.phases.cold, originalEsPolicy.phases.cold); + } + + if (policy.phases.delete.phaseEnabled) { + serializedPolicy.phases.delete = deletePhaseToES( + policy.phases.delete, + originalEsPolicy.phases.delete + ); + } + return serializedPolicy; +}; 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 new file mode 100644 index 0000000000000..545488be2cd5e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -0,0 +1,191 @@ +/* + * 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 { validateHotPhase } from './hot_phase'; +import { validateWarmPhase } from './warm_phase'; +import { validateColdPhase } from './cold_phase'; +import { validateDeletePhase } from './delete_phase'; +import { ColdPhase, DeletePhase, HotPhase, Phase, Policy, PolicyFromES, WarmPhase } from './types'; + +export const propertyof = (propertyName: keyof T & string) => propertyName; + +export const numberRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.numberRequiredError', + { + defaultMessage: 'A number is required.', + } +); + +// TODO validation includes 0 -> should be non-negative number? +export const positiveNumberRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError', + { + defaultMessage: 'Only positive numbers are allowed.', + } +); + +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', + { + defaultMessage: 'Only numbers above 0 are allowed.', + } +); + +export const policyNameRequiredMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError', + { + defaultMessage: 'A policy name is required.', + } +); + +export const policyNameStartsWithUnderscoreErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError', + { + defaultMessage: 'A policy name cannot start with an underscore.', + } +); +export const policyNameContainsCommaErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError', + { + defaultMessage: 'A policy name cannot include a comma.', + } +); +export const policyNameContainsSpaceErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError', + { + defaultMessage: 'A policy name cannot include a space.', + } +); + +export const policyNameTooLongErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError', + { + defaultMessage: 'A policy name cannot be longer than 255 bytes.', + } +); +export const policyNameMustBeDifferentErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError', + { + defaultMessage: 'The policy name must be different.', + } +); +export const policyNameAlreadyUsedErrorMessage = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError', + { + defaultMessage: 'That policy name is already used.', + } +); +export type PhaseValidationErrors = { + [P in keyof Partial]: string[]; +}; + +export interface ValidationErrors { + hot: PhaseValidationErrors; + warm: PhaseValidationErrors; + cold: PhaseValidationErrors; + delete: PhaseValidationErrors; + policyName: string[]; +} + +export const validatePolicy = ( + saveAsNew: boolean, + policy: Policy, + policies: PolicyFromES[], + originalPolicyName: string +): [boolean, ValidationErrors] => { + const policyNameErrors: string[] = []; + if (!policy.name) { + policyNameErrors.push(policyNameRequiredMessage); + } else { + if (policy.name.startsWith('_')) { + policyNameErrors.push(policyNameStartsWithUnderscoreErrorMessage); + } + if (policy.name.includes(',')) { + policyNameErrors.push(policyNameContainsCommaErrorMessage); + } + if (policy.name.includes(' ')) { + policyNameErrors.push(policyNameContainsSpaceErrorMessage); + } + if (window.TextEncoder && new window.TextEncoder().encode(policy.name).length > 255) { + policyNameErrors.push(policyNameTooLongErrorMessage); + } + + if (saveAsNew && policy.name === originalPolicyName) { + policyNameErrors.push(policyNameMustBeDifferentErrorMessage); + } else if (policy.name !== originalPolicyName) { + const policyNames = policies.map((existingPolicy) => existingPolicy.name); + if (policyNames.includes(policy.name)) { + policyNameErrors.push(policyNameAlreadyUsedErrorMessage); + } + } + } + + 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; + return [ + isValid, + { + policyName: [...policyNameErrors], + hot: hotPhaseErrors, + warm: warmPhaseErrors, + cold: coldPhaseErrors, + delete: deletePhaseErrors, + }, + ]; +}; + +export const findFirstError = (errors?: ValidationErrors): string | undefined => { + if (!errors) { + return; + } + + if (errors.policyName.length > 0) { + 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]}`; + } + if (Object.keys(errors.cold).length > 0) { + return `${propertyof('cold')}.${Object.keys(errors.cold)[0]}`; + } + if (Object.keys(errors.delete).length > 0) { + return `${propertyof('delete')}.${Object.keys(errors.delete)[0]}`; + } +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts new file mode 100644 index 0000000000000..2e2ed5b38bb87 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts @@ -0,0 +1,140 @@ +/* + * 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 interface SerializedPolicy { + name: string; + phases: Phases; +} + +export interface Phases { + hot?: SerializedHotPhase; + warm?: SerializedWarmPhase; + cold?: SerializedColdPhase; + delete?: SerializedDeletePhase; +} + +export interface PolicyFromES { + modified_date: string; + name: string; + policy: SerializedPolicy; + version: number; +} + +export interface SerializedPhase { + min_age: string; + actions: { + [action: string]: any; + }; +} + +export interface SerializedHotPhase extends SerializedPhase { + actions: { + rollover?: { + max_size?: string; + max_age?: string; + max_docs?: number; + }; + set_priority?: { + priority: number | null; + }; + }; +} + +export interface SerializedWarmPhase extends SerializedPhase { + actions: { + allocate?: AllocateAction; + shrink?: { + number_of_shards: number; + }; + forcemerge?: { + max_num_segments: number; + }; + set_priority?: { + priority: number | null; + }; + }; +} + +export interface SerializedColdPhase extends SerializedPhase { + actions: { + freeze?: {}; + allocate?: AllocateAction; + set_priority?: { + priority: number | null; + }; + }; +} + +export interface SerializedDeletePhase extends SerializedPhase { + actions: { + wait_for_snapshot?: { + policy: string; + }; + delete?: { + delete_searchable_snapshot: boolean; + }; + }; +} + +export interface AllocateAction { + number_of_replicas: number; + include: {}; + exclude: {}; + require: { + [attribute: string]: string; + }; +} + +export interface Policy { + name: string; + phases: { + hot: HotPhase; + warm: WarmPhase; + cold: ColdPhase; + delete: DeletePhase; + }; +} + +export interface Phase { + phaseEnabled: boolean; +} +export interface HotPhase extends Phase { + rolloverEnabled: boolean; + selectedMaxSizeStored: string; + selectedMaxSizeStoredUnits: string; + selectedMaxDocuments: string; + selectedMaxAge: string; + selectedMaxAgeUnits: string; + phaseIndexPriority: string; +} + +export interface WarmPhase extends Phase { + warmPhaseOnRollover: boolean; + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; + selectedNodeAttrs: string; + selectedReplicaCount: string; + shrinkEnabled: boolean; + selectedPrimaryShardCount: string; + forceMergeEnabled: boolean; + selectedForceMergeSegments: string; + phaseIndexPriority: string; +} + +export interface ColdPhase extends Phase { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; + selectedNodeAttrs: string; + selectedReplicaCount: string; + freezeEnabled: boolean; + phaseIndexPriority: string; +} + +export interface DeletePhase extends Phase { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; + waitForSnapshotPolicy: string; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts new file mode 100644 index 0000000000000..3ca1a1cc83371 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts @@ -0,0 +1,219 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { serializedPhaseInitialization } from '../../constants'; +import { AllocateAction, WarmPhase, SerializedWarmPhase } from './types'; +import { isNumber, splitSizeAndUnits } from './policy_serialization'; + +import { + numberRequiredMessage, + PhaseValidationErrors, + positiveNumberRequiredMessage, + positiveNumbersAboveZeroErrorMessage, +} from './policy_validation'; + +const warmPhaseInitialization: WarmPhase = { + phaseEnabled: false, + warmPhaseOnRollover: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + shrinkEnabled: false, + selectedPrimaryShardCount: '', + forceMergeEnabled: false, + selectedForceMergeSegments: '', + phaseIndexPriority: '', +}; + +export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhase => { + const phase: WarmPhase = { ...warmPhaseInitialization }; + + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + if (phaseSerialized.min_age === '0ms') { + phase.warmPhaseOnRollover = true; + } else { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + } + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.forcemerge) { + const forcemerge = actions.forcemerge; + phase.forceMergeEnabled = true; + phase.selectedForceMergeSegments = forcemerge.max_num_segments.toString(); + } + + if (actions.shrink) { + phase.shrinkEnabled = true; + phase.selectedPrimaryShardCount = actions.shrink.number_of_shards + ? actions.shrink.number_of_shards.toString() + : ''; + } + } + return phase; +}; + +export const warmPhaseToES = ( + phase: WarmPhase, + originalEsPhase?: SerializedWarmPhase +): SerializedWarmPhase => { + if (!originalEsPhase) { + originalEsPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalEsPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + // If warm phase on rollover is enabled, delete min age field + // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time + // They are mutually exclusive + if (phase.warmPhaseOnRollover) { + delete esPhase.min_age; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.forceMergeEnabled) { + esPhase.actions.forcemerge = { + max_num_segments: parseInt(phase.selectedForceMergeSegments, 10), + }; + } else { + delete esPhase.actions.forcemerge; + } + + if (phase.shrinkEnabled && isNumber(phase.selectedPrimaryShardCount)) { + esPhase.actions.shrink = { + number_of_shards: parseInt(phase.selectedPrimaryShardCount, 10), + }; + } else { + delete esPhase.actions.shrink; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateWarmPhase = (phase: WarmPhase): 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 warm phase on rollover is disabled, min age needs to be a positive number + if (!phase.warmPhaseOnRollover) { + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + } + + // 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]; + } + } + + // if shrink is enabled, primary shard count needs to be a number above zero + if (phase.shrinkEnabled) { + if (!isNumber(phase.selectedPrimaryShardCount)) { + phaseErrors.selectedPrimaryShardCount = [numberRequiredMessage]; + } else if (parseInt(phase.selectedPrimaryShardCount, 10) < 1) { + phaseErrors.selectedPrimaryShardCount = [positiveNumbersAboveZeroErrorMessage]; + } + } + + // replica count is optional, but if it's set, it needs to be a positive number + if (phase.selectedReplicaCount) { + if (!isNumber(phase.selectedReplicaCount)) { + phaseErrors.selectedReplicaCount = [numberRequiredMessage]; + } else if (parseInt(phase.selectedReplicaCount, 10) < 0) { + phaseErrors.selectedReplicaCount = [numberRequiredMessage]; + } + } + + return { + ...phaseErrors, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts similarity index 75% rename from x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts index 99e6bfb99472c..7c7c0b70c0eed 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts @@ -5,14 +5,13 @@ */ import { - PHASE_INDEX_PRIORITY, UIM_CONFIG_COLD_PHASE, UIM_CONFIG_WARM_PHASE, UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_FREEZE_INDEX, -} from '../constants'; - -import { defaultColdPhase, defaultWarmPhase } from '../store/defaults'; + defaultNewWarmPhase, + defaultNewColdPhase, +} from '../constants/'; import { getUiMetricsForPhases } from './ui_metric'; jest.mock('ui/new_platform'); @@ -22,9 +21,10 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ cold: { + min_age: '0ms', actions: { set_priority: { - priority: defaultColdPhase[PHASE_INDEX_PRIORITY], + priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), }, }, }, @@ -36,9 +36,10 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ warm: { + min_age: '0ms', actions: { set_priority: { - priority: defaultWarmPhase[PHASE_INDEX_PRIORITY], + priority: parseInt(defaultNewWarmPhase.phaseIndexPriority, 10), }, }, }, @@ -50,9 +51,10 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ warm: { + min_age: '0ms', actions: { set_priority: { - priority: defaultWarmPhase[PHASE_INDEX_PRIORITY] + 1, + priority: parseInt(defaultNewWarmPhase.phaseIndexPriority, 10) + 1, }, }, }, @@ -64,10 +66,11 @@ describe('getUiMetricsForPhases', () => { expect( getUiMetricsForPhases({ cold: { + min_age: '0ms', actions: { freeze: {}, set_priority: { - priority: defaultColdPhase[PHASE_INDEX_PRIORITY], + priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), }, }, }, 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 d71e38d0b31de..b38a734770546 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 @@ -4,24 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; - import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { UiStatsMetricType } from '@kbn/analytics'; import { UIM_APP_NAME, UIM_CONFIG_COLD_PHASE, - UIM_CONFIG_WARM_PHASE, - UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_FREEZE_INDEX, - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_INDEX_PRIORITY, + UIM_CONFIG_SET_PRIORITY, + UIM_CONFIG_WARM_PHASE, + defaultNewColdPhase, + defaultNewHotPhase, + defaultNewWarmPhase, } from '../constants'; -import { defaultColdPhase, defaultWarmPhase, defaultHotPhase } from '../store/defaults'; +import { Phases } from './policies/types'; export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {}; @@ -31,49 +28,54 @@ export function init(usageCollection?: UsageCollectionSetup): void { } } -export function getUiMetricsForPhases(phases: any): any { +export function getUiMetricsForPhases(phases: Phases): any { const phaseUiMetrics = [ { metric: UIM_CONFIG_COLD_PHASE, - isTracked: () => Boolean(phases[PHASE_COLD]), + isTracked: () => Boolean(phases.cold), }, { metric: UIM_CONFIG_WARM_PHASE, - isTracked: () => Boolean(phases[PHASE_WARM]), + isTracked: () => Boolean(phases.warm), }, { metric: UIM_CONFIG_SET_PRIORITY, isTracked: () => { - const phaseToDefaultIndexPriorityMap = { - [PHASE_HOT]: defaultHotPhase[PHASE_INDEX_PRIORITY], - [PHASE_WARM]: defaultWarmPhase[PHASE_INDEX_PRIORITY], - [PHASE_COLD]: defaultColdPhase[PHASE_INDEX_PRIORITY], - }; - // We only care about whether the user has interacted with the priority of *any* phase at all. - return [PHASE_HOT, PHASE_WARM, PHASE_COLD].some((phase) => { - // If the priority is different than the default, we'll consider it a user interaction, - // even if the user has set it to undefined. - return ( - phases[phase] && - get(phases[phase], 'actions.set_priority.priority') !== - phaseToDefaultIndexPriorityMap[phase] - ); - }); + const isHotPhasePriorityChanged = + phases.hot && + phases.hot.actions.set_priority && + phases.hot.actions.set_priority.priority !== + parseInt(defaultNewHotPhase.phaseIndexPriority, 10); + + const isWarmPhasePriorityChanged = + phases.warm && + phases.warm.actions.set_priority && + phases.warm.actions.set_priority.priority !== + parseInt(defaultNewWarmPhase.phaseIndexPriority, 10); + + const isColdPhasePriorityChanged = + phases.cold && + phases.cold.actions.set_priority && + phases.cold.actions.set_priority.priority !== + parseInt(defaultNewColdPhase.phaseIndexPriority, 10); + // If the priority is different than the default, we'll consider it a user interaction, + // even if the user has set it to undefined. + return ( + isHotPhasePriorityChanged || isWarmPhasePriorityChanged || isColdPhasePriorityChanged + ); }, }, { metric: UIM_CONFIG_FREEZE_INDEX, - isTracked: () => phases[PHASE_COLD] && get(phases[PHASE_COLD], 'actions.freeze'), + isTracked: () => phases.cold && phases.cold.actions.freeze, }, ]; - const trackedUiMetrics = phaseUiMetrics.reduce((tracked: any, { metric, isTracked }) => { + return phaseUiMetrics.reduce((tracked: any, { metric, isTracked }) => { if (isTracked()) { tracked.push(metric); } return tracked; }, []); - - return trackedUiMetrics; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js deleted file mode 100644 index 28719fde87b0c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/general.js +++ /dev/null @@ -1,11 +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 { createAction } from 'redux-actions'; - -export const setBootstrapEnabled = createAction('SET_BOOTSTRAP_ENABLED'); -export const setIndexName = createAction('SET_INDEX_NAME'); -export const setAliasName = createAction('SET_ALIAS_NAME'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js index ea539578c885c..fef79c7782bb0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './nodes'; export * from './policies'; -export * from './lifecycle'; -export * from './general'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js deleted file mode 100644 index 45a8e63f70e83..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js +++ /dev/null @@ -1,8 +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 { createAction } from 'redux-actions'; -export const setSelectedPrimaryShardCount = createAction('SET_SELECTED_PRIMARY_SHARED_COUNT'); -export const setSelectedReplicaCount = createAction('SET_SELECTED_REPLICA_COUNT'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js index aa20c0eb1d326..d47136679604f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js @@ -9,7 +9,6 @@ import { createAction } from 'redux-actions'; import { showApiError } from '../../services/api_errors'; import { loadPolicies } from '../../services/api'; -import { SET_PHASE_DATA } from '../../constants'; export const fetchedPolicies = createAction('FETCHED_POLICIES'); export const setSelectedPolicy = createAction('SET_SELECTED_POLICY'); @@ -41,9 +40,3 @@ export const fetchPolicies = (withIndices, callback) => async (dispatch) => { callback && callback(); return policies; }; - -export const setPhaseData = createAction(SET_PHASE_DATA, (phase, key, value) => ({ - phase, - key, - value, -})); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js deleted file mode 100644 index a8f7fd3f4bdfa..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/cold_phase.js +++ /dev/null @@ -1,30 +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 { - PHASE_ENABLED, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_NODE_ATTRS, - PHASE_REPLICA_COUNT, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ALIAS, - PHASE_FREEZE_ENABLED, - PHASE_INDEX_PRIORITY, -} from '../../constants'; - -export const defaultColdPhase = { - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ALIAS]: '', - [PHASE_ROLLOVER_MINIMUM_AGE]: 0, - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd', - [PHASE_NODE_ATTRS]: '', - [PHASE_REPLICA_COUNT]: '', - [PHASE_FREEZE_ENABLED]: false, - [PHASE_INDEX_PRIORITY]: 0, -}; -export const defaultEmptyColdPhase = { - ...defaultColdPhase, - [PHASE_INDEX_PRIORITY]: '', -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js deleted file mode 100644 index 8534893e7e3b3..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/delete_phase.js +++ /dev/null @@ -1,23 +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 { - PHASE_ENABLED, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ALIAS, - PHASE_WAIT_FOR_SNAPSHOT_POLICY, -} from '../../constants'; - -export const defaultDeletePhase = { - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ENABLED]: false, - [PHASE_ROLLOVER_ALIAS]: '', - [PHASE_ROLLOVER_MINIMUM_AGE]: 0, - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd', - [PHASE_WAIT_FOR_SNAPSHOT_POLICY]: '', -}; -export const defaultEmptyDeletePhase = defaultDeletePhase; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js deleted file mode 100644 index 1f5b5c399a642..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/hot_phase.js +++ /dev/null @@ -1,35 +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 { - PHASE_ENABLED, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_AGE_UNITS, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_DOCUMENTS, - PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, - PHASE_INDEX_PRIORITY, -} from '../../constants'; - -export const defaultHotPhase = { - [PHASE_ENABLED]: true, - [PHASE_ROLLOVER_ENABLED]: true, - [PHASE_ROLLOVER_MAX_AGE]: 30, - [PHASE_ROLLOVER_MAX_AGE_UNITS]: 'd', - [PHASE_ROLLOVER_MAX_SIZE_STORED]: 50, - [PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]: 'gb', - [PHASE_INDEX_PRIORITY]: 100, - [PHASE_ROLLOVER_MAX_DOCUMENTS]: '', -}; -export const defaultEmptyHotPhase = { - ...defaultHotPhase, - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ENABLED]: false, - [PHASE_ROLLOVER_MAX_AGE]: '', - [PHASE_ROLLOVER_MAX_SIZE_STORED]: '', - [PHASE_INDEX_PRIORITY]: '', - [PHASE_ROLLOVER_MAX_DOCUMENTS]: '', -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts deleted file mode 100644 index abf6db416c7f4..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.d.ts +++ /dev/null @@ -1,10 +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. - */ - -export declare const defaultDeletePhase: any; -export declare const defaultColdPhase: any; -export declare const defaultWarmPhase: any; -export declare const defaultHotPhase: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js deleted file mode 100644 index f5661eae91a8c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/index.js +++ /dev/null @@ -1,10 +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. - */ - -export * from './delete_phase'; -export * from './cold_phase'; -export * from './hot_phase'; -export * from './warm_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js deleted file mode 100644 index f02ac2096675f..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/defaults/warm_phase.js +++ /dev/null @@ -1,39 +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 { - PHASE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_FORCE_MERGE_ENABLED, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_NODE_ATTRS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ALIAS, - PHASE_SHRINK_ENABLED, - WARM_PHASE_ON_ROLLOVER, - PHASE_INDEX_PRIORITY, -} from '../../constants'; - -export const defaultWarmPhase = { - [PHASE_ENABLED]: false, - [PHASE_ROLLOVER_ALIAS]: '', - [PHASE_FORCE_MERGE_SEGMENTS]: '', - [PHASE_FORCE_MERGE_ENABLED]: false, - [PHASE_ROLLOVER_MINIMUM_AGE]: 0, - [PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd', - [PHASE_NODE_ATTRS]: '', - [PHASE_SHRINK_ENABLED]: false, - [PHASE_PRIMARY_SHARD_COUNT]: '', - [PHASE_REPLICA_COUNT]: '', - [WARM_PHASE_ON_ROLLOVER]: true, - [PHASE_INDEX_PRIORITY]: 50, -}; -export const defaultEmptyWarmPhase = { - ...defaultWarmPhase, - [WARM_PHASE_ON_ROLLOVER]: false, - [PHASE_INDEX_PRIORITY]: '', -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js deleted file mode 100644 index fcba2fd1358b0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/general.js +++ /dev/null @@ -1,38 +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 { handleActions } from 'redux-actions'; -import { setIndexName, setAliasName, setBootstrapEnabled } from '../actions/general'; - -const defaultState = { - bootstrapEnabled: false, - indexName: '', - aliasName: '', -}; - -export const general = handleActions( - { - [setIndexName](state, { payload: indexName }) { - return { - ...state, - indexName, - }; - }, - [setAliasName](state, { payload: aliasName }) { - return { - ...state, - aliasName, - }; - }, - [setBootstrapEnabled](state, { payload: bootstrapEnabled }) { - return { - ...state, - bootstrapEnabled, - }; - }, - }, - defaultState -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js index 60126b85c313e..7fe7134f5f5db 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js @@ -5,12 +5,8 @@ */ import { combineReducers } from 'redux'; -import { nodes } from './nodes'; import { policies } from './policies'; -import { general } from './general'; export const indexLifecycleManagement = combineReducers({ - nodes, policies, - general, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js deleted file mode 100644 index 383e61b5aacde..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/nodes.js +++ /dev/null @@ -1,50 +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 { handleActions } from 'redux-actions'; -import { setSelectedPrimaryShardCount, setSelectedReplicaCount } from '../actions'; - -const defaultState = { - isLoading: false, - selectedNodeAttrs: '', - selectedPrimaryShardCount: 1, - selectedReplicaCount: 1, - nodes: undefined, - details: {}, -}; - -export const nodes = handleActions( - { - [setSelectedPrimaryShardCount](state, { payload }) { - let selectedPrimaryShardCount = parseInt(payload); - if (isNaN(selectedPrimaryShardCount)) { - selectedPrimaryShardCount = ''; - } - return { - ...state, - selectedPrimaryShardCount, - }; - }, - [setSelectedReplicaCount](state, { payload }) { - let selectedReplicaCount; - if (payload != null) { - selectedReplicaCount = parseInt(payload); - if (isNaN(selectedReplicaCount)) { - selectedReplicaCount = ''; - } - } else { - // default value for Elasticsearch - selectedReplicaCount = 1; - } - - return { - ...state, - selectedReplicaCount, - }; - }, - }, - defaultState -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js index a94e875a71845..ca9d59e295a29 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js @@ -7,49 +7,17 @@ import { handleActions } from 'redux-actions'; import { fetchedPolicies, - setSelectedPolicy, - unsetSelectedPolicy, - setSelectedPolicyName, - setSaveAsNewPolicy, - setPhaseData, policyFilterChanged, policyPageChanged, policyPageSizeChanged, policySortChanged, } from '../actions'; -import { policyFromES } from '../selectors'; -import { - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, - PHASE_ATTRIBUTES_THAT_ARE_NUMBERS, -} from '../../constants'; - -import { - defaultColdPhase, - defaultDeletePhase, - defaultHotPhase, - defaultWarmPhase, -} from '../defaults'; -export const defaultPolicy = { - name: '', - saveAsNew: true, - isNew: true, - phases: { - [PHASE_HOT]: defaultHotPhase, - [PHASE_WARM]: defaultWarmPhase, - [PHASE_COLD]: defaultColdPhase, - [PHASE_DELETE]: defaultDeletePhase, - }, -}; const defaultState = { isLoading: false, isLoaded: false, originalPolicyName: undefined, selectedPolicySet: false, - selectedPolicy: defaultPolicy, policies: [], sort: { sortField: 'name', @@ -70,71 +38,6 @@ export const policies = handleActions( policies, }; }, - [setSelectedPolicy](state, { payload: selectedPolicy }) { - if (!selectedPolicy) { - return { - ...state, - selectedPolicy: defaultPolicy, - selectedPolicySet: true, - }; - } - - return { - ...state, - originalPolicyName: selectedPolicy.name, - selectedPolicySet: true, - selectedPolicy: { - ...defaultPolicy, - ...policyFromES(selectedPolicy), - }, - }; - }, - [unsetSelectedPolicy]() { - return defaultState; - }, - [setSelectedPolicyName](state, { payload: name }) { - return { - ...state, - selectedPolicy: { - ...state.selectedPolicy, - name, - }, - }; - }, - [setSaveAsNewPolicy](state, { payload: saveAsNew }) { - return { - ...state, - selectedPolicy: { - ...state.selectedPolicy, - saveAsNew, - }, - }; - }, - [setPhaseData](state, { payload }) { - const { phase, key } = payload; - - let value = payload.value; - if (PHASE_ATTRIBUTES_THAT_ARE_NUMBERS.includes(key)) { - value = parseInt(value); - if (isNaN(value)) { - value = ''; - } - } - - return { - ...state, - selectedPolicy: { - ...state.selectedPolicy, - phases: { - ...state.selectedPolicy.phases, - [phase]: { - ...state.selectedPolicy.phases[phase], - [key]: value, - }, - }, - }, - }; - }, [policyFilterChanged](state, action) { const { filter } = action.payload; return { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js deleted file mode 100644 index 2d01749be3087..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/general.js +++ /dev/null @@ -1,9 +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. - */ - -export const getBootstrapEnabled = (state) => state.general.bootstrapEnabled; -export const getIndexName = (state) => state.general.indexName; -export const getAliasName = (state) => state.general.aliasName; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js index ea539578c885c..fef79c7782bb0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './nodes'; export * from './policies'; -export * from './lifecycle'; -export * from './general'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js deleted file mode 100644 index 03538fad9aa83..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/lifecycle.js +++ /dev/null @@ -1,287 +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 { i18n } from '@kbn/i18n'; - -import { - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, - PHASE_ENABLED, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MAX_SIZE_STORED, - STRUCTURE_POLICY_NAME, - ERROR_STRUCTURE, - PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_SHRINK_ENABLED, - PHASE_FORCE_MERGE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_REPLICA_COUNT, - WARM_PHASE_ON_ROLLOVER, - PHASE_INDEX_PRIORITY, - PHASE_ROLLOVER_MAX_DOCUMENTS, -} from '../../constants'; - -import { - getPhase, - getPhases, - phaseToES, - getSelectedPolicyName, - isNumber, - getSaveAsNewPolicy, - getSelectedOriginalPolicyName, - getPolicies, -} from '.'; - -import { getPolicyByName } from './policies'; - -export const numberRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.numberRequiredError', - { - defaultMessage: 'A number is required.', - } -); - -export const positiveNumberRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError', - { - defaultMessage: 'Only positive numbers are allowed.', - } -); - -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', - { - defaultMessage: 'Only numbers above 0 are allowed.', - } -); - -export const validatePhase = (type, phase, errors) => { - const phaseErrors = {}; - - if (!phase[PHASE_ENABLED]) { - return; - } - - for (const numberedAttribute of PHASE_ATTRIBUTES_THAT_ARE_NUMBERS_VALIDATE) { - if (phase.hasOwnProperty(numberedAttribute)) { - // If WARM_PHASE_ON_ROLLOVER or PHASE_HOT there is no need to validate this - if ( - numberedAttribute === PHASE_ROLLOVER_MINIMUM_AGE && - (phase[WARM_PHASE_ON_ROLLOVER] || type === PHASE_HOT) - ) { - continue; - } - // If shrink is disabled, there is no need to validate this - if (numberedAttribute === PHASE_PRIMARY_SHARD_COUNT && !phase[PHASE_SHRINK_ENABLED]) { - continue; - } - // If forcemerge is disabled, there is no need to validate this - if (numberedAttribute === PHASE_FORCE_MERGE_SEGMENTS && !phase[PHASE_FORCE_MERGE_ENABLED]) { - continue; - } - // PHASE_REPLICA_COUNT is optional and can be zero - if (numberedAttribute === PHASE_REPLICA_COUNT && !phase[numberedAttribute]) { - continue; - } - // PHASE_INDEX_PRIORITY is optional and can be zero - if (numberedAttribute === PHASE_INDEX_PRIORITY && !phase[numberedAttribute]) { - continue; - } - if (!isNumber(phase[numberedAttribute])) { - phaseErrors[numberedAttribute] = [numberRequiredMessage]; - } else if (phase[numberedAttribute] < 0) { - phaseErrors[numberedAttribute] = [positiveNumberRequiredMessage]; - } - } - } - if (phase[PHASE_ROLLOVER_ENABLED]) { - if ( - !isNumber(phase[PHASE_ROLLOVER_MAX_AGE]) && - !isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED]) && - !isNumber(phase[PHASE_ROLLOVER_MAX_DOCUMENTS]) - ) { - phaseErrors[PHASE_ROLLOVER_MAX_AGE] = [maximumAgeRequiredMessage]; - phaseErrors[PHASE_ROLLOVER_MAX_SIZE_STORED] = [maximumSizeRequiredMessage]; - phaseErrors[PHASE_ROLLOVER_MAX_DOCUMENTS] = [maximumDocumentsRequiredMessage]; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_AGE]) && phase[PHASE_ROLLOVER_MAX_AGE] < 1) { - phaseErrors[PHASE_ROLLOVER_MAX_AGE] = [positiveNumbersAboveZeroErrorMessage]; - } - if ( - isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED]) && - phase[PHASE_ROLLOVER_MAX_SIZE_STORED] < 1 - ) { - phaseErrors[PHASE_ROLLOVER_MAX_SIZE_STORED] = [positiveNumbersAboveZeroErrorMessage]; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_DOCUMENTS]) && phase[PHASE_ROLLOVER_MAX_DOCUMENTS] < 1) { - phaseErrors[PHASE_ROLLOVER_MAX_DOCUMENTS] = [positiveNumbersAboveZeroErrorMessage]; - } - } - if (phase[PHASE_SHRINK_ENABLED]) { - if (!isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) { - phaseErrors[PHASE_PRIMARY_SHARD_COUNT] = [numberRequiredMessage]; - } else if (phase[PHASE_PRIMARY_SHARD_COUNT] < 1) { - phaseErrors[PHASE_PRIMARY_SHARD_COUNT] = [positiveNumbersAboveZeroErrorMessage]; - } - } - - if (phase[PHASE_FORCE_MERGE_ENABLED]) { - if (!isNumber(phase[PHASE_FORCE_MERGE_SEGMENTS])) { - phaseErrors[PHASE_FORCE_MERGE_SEGMENTS] = [numberRequiredMessage]; - } else if (phase[PHASE_FORCE_MERGE_SEGMENTS] < 1) { - phaseErrors[PHASE_FORCE_MERGE_SEGMENTS] = [positiveNumbersAboveZeroErrorMessage]; - } - } - errors[type] = { - ...errors[type], - ...phaseErrors, - }; -}; - -export const policyNameRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError', - { - defaultMessage: 'A policy name is required.', - } -); -export const policyNameStartsWithUnderscoreErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError', - { - defaultMessage: 'A policy name cannot start with an underscore.', - } -); -export const policyNameContainsCommaErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError', - { - defaultMessage: 'A policy name cannot include a comma.', - } -); -export const policyNameContainsSpaceErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError', - { - defaultMessage: 'A policy name cannot include a space.', - } -); -export const policyNameTooLongErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError', - { - defaultMessage: 'A policy name cannot be longer than 255 bytes.', - } -); -export const policyNameMustBeDifferentErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError', - { - defaultMessage: 'The policy name must be different.', - } -); -export const policyNameAlreadyUsedErrorMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError', - { - defaultMessage: 'That policy name is already used.', - } -); -export const validateLifecycle = (state) => { - // This method of deep copy does not always work but it should be fine here - const errors = JSON.parse(JSON.stringify(ERROR_STRUCTURE)); - const policyName = getSelectedPolicyName(state); - if (!policyName) { - errors[STRUCTURE_POLICY_NAME].push(policyNameRequiredMessage); - } else { - if (policyName.startsWith('_')) { - errors[STRUCTURE_POLICY_NAME].push(policyNameStartsWithUnderscoreErrorMessage); - } - if (policyName.includes(',')) { - errors[STRUCTURE_POLICY_NAME].push(policyNameContainsCommaErrorMessage); - } - if (policyName.includes(' ')) { - errors[STRUCTURE_POLICY_NAME].push(policyNameContainsSpaceErrorMessage); - } - if (window.TextEncoder && new window.TextEncoder('utf-8').encode(policyName).length > 255) { - errors[STRUCTURE_POLICY_NAME].push(policyNameTooLongErrorMessage); - } - } - - if ( - getSaveAsNewPolicy(state) && - getSelectedOriginalPolicyName(state) === getSelectedPolicyName(state) - ) { - errors[STRUCTURE_POLICY_NAME].push(policyNameMustBeDifferentErrorMessage); - } else if (getSelectedOriginalPolicyName(state) !== getSelectedPolicyName(state)) { - const policyNames = getPolicies(state).map((policy) => policy.name); - if (policyNames.includes(getSelectedPolicyName(state))) { - errors[STRUCTURE_POLICY_NAME].push(policyNameAlreadyUsedErrorMessage); - } - } - - const hotPhase = getPhase(state, PHASE_HOT); - const warmPhase = getPhase(state, PHASE_WARM); - const coldPhase = getPhase(state, PHASE_COLD); - const deletePhase = getPhase(state, PHASE_DELETE); - - validatePhase(PHASE_HOT, hotPhase, errors); - validatePhase(PHASE_WARM, warmPhase, errors); - validatePhase(PHASE_COLD, coldPhase, errors); - validatePhase(PHASE_DELETE, deletePhase, errors); - return errors; -}; - -export const getLifecycle = (state) => { - const policyName = getSelectedPolicyName(state); - const phases = Object.entries(getPhases(state)).reduce((accum, [phaseName, phase]) => { - // Hot is ALWAYS enabled - if (phaseName === PHASE_HOT) { - phase[PHASE_ENABLED] = true; - } - const esPolicy = getPolicyByName(state, policyName).policy || {}; - const esPhase = esPolicy.phases ? esPolicy.phases[phaseName] : {}; - if (phase[PHASE_ENABLED]) { - accum[phaseName] = phaseToES(phase, esPhase); - - // These seem to be constants - if (phaseName === PHASE_DELETE) { - accum[phaseName].actions = { - ...accum[phaseName].actions, - delete: { - ...accum[phaseName].actions.delete, - }, - }; - } - } - return accum; - }, {}); - - return { - name: getSelectedPolicyName(state), - //type, TODO: figure this out (jsut store it and not let the user change it?) - phases, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js deleted file mode 100644 index 72bfd4b15a78a..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/nodes.js +++ /dev/null @@ -1,12 +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. - */ - -export const getNodes = (state) => state.nodes.nodes; - -export const getSelectedPrimaryShardCount = (state) => state.nodes.selectedPrimaryShardCount; - -export const getSelectedReplicaCount = (state) => - state.nodes.selectedReplicaCount !== undefined ? state.nodes.selectedReplicaCount : 1; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js index 5bea22f0b3a76..e1c89314a2ec5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js @@ -7,49 +7,9 @@ import { createSelector } from 'reselect'; import { Pager } from '@elastic/eui'; -import { - PHASE_HOT, - PHASE_WARM, - PHASE_COLD, - PHASE_DELETE, - PHASE_ROLLOVER_MINIMUM_AGE, - PHASE_ROLLOVER_MINIMUM_AGE_UNITS, - PHASE_ROLLOVER_ENABLED, - PHASE_ROLLOVER_MAX_AGE, - PHASE_ROLLOVER_MAX_AGE_UNITS, - PHASE_ROLLOVER_MAX_SIZE_STORED, - PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, - PHASE_NODE_ATTRS, - PHASE_FORCE_MERGE_ENABLED, - PHASE_FORCE_MERGE_SEGMENTS, - PHASE_PRIMARY_SHARD_COUNT, - PHASE_REPLICA_COUNT, - PHASE_ENABLED, - PHASE_ATTRIBUTES_THAT_ARE_NUMBERS, - WARM_PHASE_ON_ROLLOVER, - PHASE_SHRINK_ENABLED, - PHASE_FREEZE_ENABLED, - PHASE_INDEX_PRIORITY, - PHASE_ROLLOVER_MAX_DOCUMENTS, - PHASE_WAIT_FOR_SNAPSHOT_POLICY, -} from '../../constants'; - import { filterItems, sortTable } from '../../services'; -import { - defaultEmptyDeletePhase, - defaultEmptyColdPhase, - defaultEmptyWarmPhase, - defaultEmptyHotPhase, -} from '../defaults'; - export const getPolicies = (state) => state.policies.policies; -export const getPolicyByName = (state, name) => - getPolicies(state).find((policy) => policy.name === name) || {}; -export const getIsNewPolicy = (state) => state.policies.selectedPolicy.isNew; -export const getSelectedPolicy = (state) => state.policies.selectedPolicy; -export const getIsSelectedPolicySet = (state) => state.policies.selectedPolicySet; -export const getSelectedOriginalPolicyName = (state) => state.policies.originalPolicyName; export const getPolicyFilter = (state) => state.policies.filter; export const getPolicySort = (state) => state.policies.sort; export const getPolicyCurrentPage = (state) => state.policies.currentPage; @@ -77,255 +37,6 @@ export const getPageOfPolicies = createSelector( (filteredPolicies, sort, pager) => { const sortedPolicies = sortTable(filteredPolicies, sort.sortField, sort.isSortAscending); const { firstItemIndex, lastItemIndex } = pager; - const pagedPolicies = sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); - return pagedPolicies; + return sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); } ); -export const getSaveAsNewPolicy = (state) => state.policies.selectedPolicy.saveAsNew; - -export const getSelectedPolicyName = (state) => { - if (!getSaveAsNewPolicy(state)) { - return getSelectedOriginalPolicyName(state); - } - return state.policies.selectedPolicy.name; -}; - -export const getPhases = (state) => state.policies.selectedPolicy.phases; - -export const getPhase = (state, phase) => getPhases(state)[phase]; - -export const getPhaseData = (state, phase, key) => { - if (PHASE_ATTRIBUTES_THAT_ARE_NUMBERS.includes(key)) { - return parseInt(getPhase(state, phase)[key]); - } - return getPhase(state, phase)[key]; -}; - -export const splitSizeAndUnits = (field) => { - let size; - let units; - - const result = /(\d+)(\w+)/.exec(field); - if (result) { - size = parseInt(result[1]) || 0; - units = result[2]; - } - - return { - size, - units, - }; -}; - -export const isNumber = (value) => typeof value === 'number'; -export const isEmptyObject = (obj) => { - return !obj || (Object.entries(obj).length === 0 && obj.constructor === Object); -}; - -const phaseFromES = (phase, phaseName, defaultEmptyPolicy) => { - const policy = { ...defaultEmptyPolicy }; - if (!phase) { - return policy; - } - - policy[PHASE_ENABLED] = true; - - if (phase.min_age) { - if (phaseName === PHASE_WARM && phase.min_age === '0ms') { - policy[WARM_PHASE_ON_ROLLOVER] = true; - } else { - const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phase.min_age); - policy[PHASE_ROLLOVER_MINIMUM_AGE] = minAge; - policy[PHASE_ROLLOVER_MINIMUM_AGE_UNITS] = minAgeUnits; - } - } - if (phaseName === PHASE_WARM) { - policy[PHASE_SHRINK_ENABLED] = false; - policy[PHASE_FORCE_MERGE_ENABLED] = false; - } - if (phase.actions) { - const actions = phase.actions; - - if (actions.rollover) { - const rollover = actions.rollover; - policy[PHASE_ROLLOVER_ENABLED] = true; - if (rollover.max_age) { - const { size: maxAge, units: maxAgeUnits } = splitSizeAndUnits(rollover.max_age); - policy[PHASE_ROLLOVER_MAX_AGE] = maxAge; - policy[PHASE_ROLLOVER_MAX_AGE_UNITS] = maxAgeUnits; - } - if (rollover.max_size) { - const { size: maxSize, units: maxSizeUnits } = splitSizeAndUnits(rollover.max_size); - policy[PHASE_ROLLOVER_MAX_SIZE_STORED] = maxSize; - policy[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS] = maxSizeUnits; - } - if (rollover.max_docs) { - policy[PHASE_ROLLOVER_MAX_DOCUMENTS] = rollover.max_docs; - } - } - - if (actions.allocate) { - const allocate = actions.allocate; - if (allocate.require) { - Object.entries(allocate.require).forEach((entry) => { - policy[PHASE_NODE_ATTRS] = entry.join(':'); - }); - // checking for null or undefined here - if (allocate.number_of_replicas != null) { - policy[PHASE_REPLICA_COUNT] = allocate.number_of_replicas; - } - } - } - - if (actions.forcemerge) { - const forcemerge = actions.forcemerge; - policy[PHASE_FORCE_MERGE_ENABLED] = true; - policy[PHASE_FORCE_MERGE_SEGMENTS] = forcemerge.max_num_segments; - } - - if (actions.shrink) { - policy[PHASE_SHRINK_ENABLED] = true; - policy[PHASE_PRIMARY_SHARD_COUNT] = actions.shrink.number_of_shards; - } - - if (actions.freeze) { - policy[PHASE_FREEZE_ENABLED] = true; - } - - if (actions.set_priority) { - const { priority } = actions.set_priority; - - policy[PHASE_INDEX_PRIORITY] = priority ?? ''; - } - - if (actions.wait_for_snapshot) { - policy[PHASE_WAIT_FOR_SNAPSHOT_POLICY] = actions.wait_for_snapshot.policy; - } - } - return policy; -}; - -export const policyFromES = (policy) => { - const { - name, - policy: { phases }, - } = policy; - - return { - name, - phases: { - [PHASE_HOT]: phaseFromES(phases[PHASE_HOT], PHASE_HOT, defaultEmptyHotPhase), - [PHASE_WARM]: phaseFromES(phases[PHASE_WARM], PHASE_WARM, defaultEmptyWarmPhase), - [PHASE_COLD]: phaseFromES(phases[PHASE_COLD], PHASE_COLD, defaultEmptyColdPhase), - [PHASE_DELETE]: phaseFromES(phases[PHASE_DELETE], PHASE_DELETE, defaultEmptyDeletePhase), - }, - isNew: false, - saveAsNew: false, - }; -}; - -export const phaseToES = (phase, originalEsPhase) => { - const esPhase = { ...originalEsPhase }; - - if (!phase[PHASE_ENABLED]) { - return {}; - } - if (isNumber(phase[PHASE_ROLLOVER_MINIMUM_AGE])) { - esPhase.min_age = `${phase[PHASE_ROLLOVER_MINIMUM_AGE]}${phase[PHASE_ROLLOVER_MINIMUM_AGE_UNITS]}`; - } - - // If warm phase on rollover is enabled, delete min age field - // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time - // They are mutually exclusive - if (phase[WARM_PHASE_ON_ROLLOVER]) { - delete esPhase.min_age; - } - - esPhase.actions = esPhase.actions || {}; - - if (phase[PHASE_ROLLOVER_ENABLED]) { - esPhase.actions.rollover = {}; - - if (isNumber(phase[PHASE_ROLLOVER_MAX_AGE])) { - esPhase.actions.rollover.max_age = `${phase[PHASE_ROLLOVER_MAX_AGE]}${phase[PHASE_ROLLOVER_MAX_AGE_UNITS]}`; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_SIZE_STORED])) { - esPhase.actions.rollover.max_size = `${phase[PHASE_ROLLOVER_MAX_SIZE_STORED]}${phase[PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS]}`; - } - if (isNumber(phase[PHASE_ROLLOVER_MAX_DOCUMENTS])) { - esPhase.actions.rollover.max_docs = phase[PHASE_ROLLOVER_MAX_DOCUMENTS]; - } - } else { - delete esPhase.actions.rollover; - } - if (phase[PHASE_NODE_ATTRS]) { - const [name, value] = phase[PHASE_NODE_ATTRS].split(':'); - esPhase.actions.allocate = esPhase.actions.allocate || {}; - esPhase.actions.allocate.require = { - [name]: value, - }; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.require; - } - } - if (isNumber(phase[PHASE_REPLICA_COUNT])) { - esPhase.actions.allocate = esPhase.actions.allocate || {}; - esPhase.actions.allocate.number_of_replicas = phase[PHASE_REPLICA_COUNT]; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.number_of_replicas; - } - } - if ( - esPhase.actions.allocate && - !esPhase.actions.allocate.require && - !isNumber(esPhase.actions.allocate.number_of_replicas) && - isEmptyObject(esPhase.actions.allocate.include) && - isEmptyObject(esPhase.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete esPhase.actions.allocate; - } - - if (phase[PHASE_FORCE_MERGE_ENABLED]) { - esPhase.actions.forcemerge = { - max_num_segments: phase[PHASE_FORCE_MERGE_SEGMENTS], - }; - } else { - delete esPhase.actions.forcemerge; - } - - if (phase[PHASE_SHRINK_ENABLED] && isNumber(phase[PHASE_PRIMARY_SHARD_COUNT])) { - esPhase.actions.shrink = { - number_of_shards: phase[PHASE_PRIMARY_SHARD_COUNT], - }; - } else { - delete esPhase.actions.shrink; - } - - if (phase[PHASE_FREEZE_ENABLED]) { - esPhase.actions.freeze = {}; - } else { - delete esPhase.actions.freeze; - } - if (isNumber(phase[PHASE_INDEX_PRIORITY])) { - esPhase.actions.set_priority = { - priority: phase[PHASE_INDEX_PRIORITY], - }; - } else if (phase[PHASE_INDEX_PRIORITY] === '') { - esPhase.actions.set_priority = { - priority: null, - }; - } - - if (phase[PHASE_WAIT_FOR_SNAPSHOT_POLICY]) { - esPhase.actions.wait_for_snapshot = { - policy: phase[PHASE_WAIT_FOR_SNAPSHOT_POLICY], - }; - } else { - delete esPhase.actions.wait_for_snapshot; - } - return esPhase; -}; diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx index 84316a1b9105d..9bab590d1f5ea 100644 --- a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx +++ b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx @@ -13,8 +13,10 @@ import { EuiDroppable, EuiText, EuiTextAlign, + EuiTextColor, EuiSpacer, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { AddTooltipFieldPopover, FieldProps } from './add_tooltip_field_popover'; import { IField } from '../../classes/fields/field'; @@ -156,7 +158,18 @@ export class TooltipSelector extends Component { _renderProperties() { if (!this.state.selectedFieldProps.length) { - return null; + return ( + + + + + + + + ); } return ( diff --git a/x-pack/plugins/security_solution/public/common/store/epic.ts b/x-pack/plugins/security_solution/public/common/store/epic.ts index d9de7951a86f4..51a9377b9fd04 100644 --- a/x-pack/plugins/security_solution/public/common/store/epic.ts +++ b/x-pack/plugins/security_solution/public/common/store/epic.ts @@ -4,14 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineEpics } from 'redux-observable'; +import { combineEpics, Epic } from 'redux-observable'; +import { Action } from 'redux'; + import { createTimelineEpic } from '../../timelines/store/timeline/epic'; import { createTimelineFavoriteEpic } from '../../timelines/store/timeline/epic_favorite'; import { createTimelineNoteEpic } from '../../timelines/store/timeline/epic_note'; import { createTimelinePinnedEventEpic } from '../../timelines/store/timeline/epic_pinned_event'; import { createTimelineLocalStorageEpic } from '../../timelines/store/timeline/epic_local_storage'; +import { TimelineEpicDependencies } from '../../timelines/store/timeline/types'; -export const createRootEpic = () => +export const createRootEpic = (): Epic< + Action, + Action, + State, + TimelineEpicDependencies +> => combineEpics( createTimelineEpic(), createTimelineFavoriteEpic(), diff --git a/x-pack/plugins/security_solution/public/common/store/store.ts b/x-pack/plugins/security_solution/public/common/store/store.ts index a39c9f18bcdb8..f041e1fd82a9f 100644 --- a/x-pack/plugins/security_solution/public/common/store/store.ts +++ b/x-pack/plugins/security_solution/public/common/store/store.ts @@ -13,6 +13,7 @@ import { Middleware, Dispatch, PreloadedState, + CombinedState, } from 'redux'; import { createEpicMiddleware } from 'redux-observable'; @@ -30,6 +31,7 @@ import { Immutable } from '../../../common/endpoint/types'; import { State } from './types'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { CoreStart } from '../../../../../../src/core/public'; +import { TimelineEpicDependencies } from '../../timelines/store/timeline/types'; type ComposeType = typeof compose; declare global { @@ -56,7 +58,7 @@ export const createStore = ( ): Store => { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const middlewareDependencies = { + const middlewareDependencies: TimelineEpicDependencies = { apolloClient$: apolloClient, kibana$: kibana, selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector, @@ -80,7 +82,7 @@ export const createStore = ( ) ); - epicMiddleware.run(createRootEpic()); + epicMiddleware.run(createRootEpic>()); return store; }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f1a933fb34d66..a691dd98e7081 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -66,7 +66,7 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { initTelemetry(plugins.usageCollection, APP_ID); plugins.home.featureCatalogue.register({ @@ -319,7 +319,12 @@ export class Plugin implements IPlugin { + const { resolverPluginSetup } = await import('./resolver'); + return resolverPluginSetup(); + }, + }; } public start(core: CoreStart, plugins: StartPlugins) { diff --git a/x-pack/plugins/security_solution/public/resolver/index.ts b/x-pack/plugins/security_solution/public/resolver/index.ts new file mode 100644 index 0000000000000..409f82c9d1560 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { Provider } from 'react-redux'; +import { ResolverPluginSetup } from './types'; +import { resolverStoreFactory } from './store/index'; +import { ResolverWithoutProviders } from './view/resolver_without_providers'; +import { noAncestorsTwoChildren } from './data_access_layer/mocks/no_ancestors_two_children'; + +/** + * These exports are used by the plugin 'resolverTest' defined in x-pack's plugin_functional suite. + */ + +/** + * Provide access to Resolver APIs. + */ +export function resolverPluginSetup(): ResolverPluginSetup { + return { + Provider, + storeFactory: resolverStoreFactory, + ResolverWithoutProviders, + mocks: { + dataAccessLayer: { + noAncestorsTwoChildren, + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/index.ts b/x-pack/plugins/security_solution/public/resolver/store/index.ts index 950a61db33f17..ed8a5129c7ff6 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/index.ts @@ -11,7 +11,7 @@ import { resolverReducer } from './reducer'; import { resolverMiddlewareFactory } from './middleware'; import { ResolverAction } from './actions'; -export const storeFactory = ( +export const resolverStoreFactory = ( dataAccessLayer: DataAccessLayer ): Store => { const actionsDenylist: Array = ['userMovedPointer']; diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index b79b7df48a6de..a6520c8f0e06f 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Store, createStore, applyMiddleware } from 'redux'; import { mount, ReactWrapper } from 'enzyme'; -import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; +import { History as HistoryPackageHistoryInterface, createMemoryHistory } from 'history'; import { CoreStart } from '../../../../../../../src/core/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { spyMiddlewareFactory } from '../spy_middleware_factory'; diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 97d97700b11ae..33f7a1d97db13 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -9,6 +9,7 @@ import { Store } from 'redux'; import { Middleware, Dispatch } from 'redux'; import { BBox } from 'rbush'; +import { Provider } from 'react-redux'; import { ResolverAction } from './store/actions'; import { ResolverRelatedEvents, @@ -410,7 +411,7 @@ export interface SideEffectSimulator { /** * Mocked `SideEffectors`. */ - mock: jest.Mocked> & Pick; + mock: SideEffectors; } /** @@ -532,3 +533,42 @@ export interface SpyMiddleware { */ debugActions: () => () => void; } + +/** + * values of this type are exposed by the Security Solution plugin's setup phase. + */ +export interface ResolverPluginSetup { + /** + * Provide access to the instance of the `react-redux` `Provider` that Resolver recognizes. + */ + Provider: typeof Provider; + /** + * Takes a `DataAccessLayer`, which could be a mock one, and returns an redux Store. + * All data acess (e.g. HTTP requests) are done through the store. + */ + storeFactory: (dataAccessLayer: DataAccessLayer) => Store; + + /** + * The Resolver component without the required Providers. + * You must wrap this component in: `I18nProvider`, `Router` (from react-router,) `KibanaContextProvider`, + * and the `Provider` component provided by this object. + */ + ResolverWithoutProviders: React.MemoExoticComponent< + React.ForwardRefExoticComponent> + >; + + /** + * A collection of mock objects that can be used in examples or in testing. + */ + mocks: { + /** + * Mock `DataAccessLayer`s. All of Resolver's HTTP access is provided by a `DataAccessLayer`. + */ + dataAccessLayer: { + /** + * A mock `DataAccessLayer` that returns a tree that has no ancestor nodes but which has 2 children nodes. + */ + noAncestorsTwoChildren: () => { dataAccessLayer: DataAccessLayer }; + }; + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index d9a0bf291d0e4..bcc420435e5d9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { Provider } from 'react-redux'; -import { storeFactory } from '../store'; +import { resolverStoreFactory } from '../store'; import { StartServices } from '../../types'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { DataAccessLayer, ResolverProps } from '../types'; @@ -24,7 +24,7 @@ export const Resolver = React.memo((props: ResolverProps) => { ]); const store = useMemo(() => { - return storeFactory(dataAccessLayer); + return resolverStoreFactory(dataAccessLayer); }, [dataAccessLayer]); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts index c64ed608339b6..8a5344e0754db 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts @@ -10,9 +10,9 @@ import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { AppApolloClient } from '../../../common/lib/lib'; import { inputsModel } from '../../../common/store/inputs'; import { NotesById } from '../../../common/store/app/model'; -import { StartServices } from '../../../types'; import { TimelineModel } from './model'; +import { CoreStart } from '../../../../../../../src/core/public'; export interface AutoSavedWarningMsg { timelineId: string | null; @@ -55,6 +55,6 @@ export interface TimelineEpicDependencies { selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; selectNotesByIdSelector: (state: State) => NotesById; apolloClient$: Observable; - kibana$: Observable; + kibana$: Observable; storage: Storage; } diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 3913b96b3e11a..fd1ff566a7719 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -21,6 +21,7 @@ import { } from '../../triggers_actions_ui/public'; import { SecurityPluginSetup } from '../../security/public'; import { AppFrontendLibs } from './common/lib/lib'; +import { ResolverPluginSetup } from './resolver/types'; export interface SetupPlugins { home: HomePublicPluginSetup; @@ -46,8 +47,9 @@ export type StartServices = CoreStart & storage: Storage; }; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginSetup {} +export interface PluginSetup { + resolver: () => Promise; +} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json index c715a0aaa3b20..499983561e89d 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json +++ b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json @@ -2,8 +2,13 @@ "id": "resolver_test", "version": "1.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "resolver_test"], - "requiredPlugins": ["embeddable"], + "configPath": ["xpack", "resolverTest"], + "requiredPlugins": [ + "securitySolution" + ], + "requiredBundles": [ + "kibanaReact" + ], "server": false, "ui": true } diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index 79665b6a393df..4afd71fd67a69 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -4,119 +4,95 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import { Router } from 'react-router-dom'; + +import React from 'react'; import ReactDOM from 'react-dom'; -import { AppMountParameters } from 'kibana/public'; -import { I18nProvider } from '@kbn/i18n/react'; -import { IEmbeddable } from 'src/plugins/embeddable/public'; -import { useEffect } from 'react'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { useMemo } from 'react'; import styled from 'styled-components'; +import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + DataAccessLayer, + ResolverPluginSetup, +} from '../../../../../../../plugins/security_solution/public/resolver/types'; /** * Render the Resolver Test app. Returns a cleanup function. */ export function renderApp( - { element }: AppMountParameters, - embeddable: Promise + coreStart: CoreStart, + parameters: AppMountParameters, + resolverPluginSetup: ResolverPluginSetup ) { /** * The application DOM node should take all available space. */ - element.style.display = 'flex'; - element.style.flexGrow = '1'; + parameters.element.style.display = 'flex'; + parameters.element.style.flexGrow = '1'; ReactDOM.render( - - - , - element + , + parameters.element ); return () => { - ReactDOM.unmountComponentAtNode(element); + ReactDOM.unmountComponentAtNode(parameters.element); }; } -const AppRoot = styled( - React.memo( - ({ - embeddable: embeddablePromise, - className, - }: { - /** - * A promise which resolves to the Resolver embeddable. - */ - embeddable: Promise; - /** - * A `className` string provided by `styled` - */ - className?: string; - }) => { - /** - * This state holds the reference to the embeddable, once resolved. - */ - const [embeddable, setEmbeddable] = React.useState(undefined); - /** - * This state holds the reference to the DOM node that will contain the embeddable. - */ - const [renderTarget, setRenderTarget] = React.useState(null); - - /** - * Keep component state with the Resolver embeddable. - * - * If the reference to the embeddablePromise changes, we ignore the stale promise. - */ - useEffect(() => { - /** - * A promise rejection function that will prevent a stale embeddable promise from being resolved - * as the current eembeddable. - * - * If the embeddablePromise itself changes before the old one is resolved, we cancel and restart this effect. - */ - let cleanUp; - - const cleanupPromise = new Promise((_resolve, reject) => { - cleanUp = reject; - }); - - /** - * Either set the embeddable in state, or cancel and restart this process. - */ - Promise.race([cleanupPromise, embeddablePromise]).then((value) => { - setEmbeddable(value); - }); +const AppRoot = React.memo( + ({ + coreStart, + parameters, + resolverPluginSetup, + }: { + coreStart: CoreStart; + parameters: AppMountParameters; + resolverPluginSetup: ResolverPluginSetup; + }) => { + const { + Provider, + storeFactory, + ResolverWithoutProviders, + mocks: { + dataAccessLayer: { noAncestorsTwoChildren }, + }, + } = resolverPluginSetup; + const dataAccessLayer: DataAccessLayer = useMemo( + () => noAncestorsTwoChildren().dataAccessLayer, + [noAncestorsTwoChildren] + ); - /** - * If `embeddablePromise` is changed, the cleanup function is run. - */ - return cleanUp; - }, [embeddablePromise]); + const store = useMemo(() => { + return storeFactory(dataAccessLayer); + }, [storeFactory, dataAccessLayer]); - /** - * Render the eembeddable into the DOM node. - */ - useEffect(() => { - if (embeddable && renderTarget) { - embeddable.render(renderTarget); - /** - * If the embeddable or DOM node changes then destroy the old embeddable. - */ - return () => { - embeddable.destroy(); - }; - } - }, [embeddable, renderTarget]); + return ( + + + + + + + + + + + + ); + } +); - return ( - - ); - } - ) -)` +const Wrapper = styled.div` /** * Take all available space. */ diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts index 853265ae6e5de..3da3044283556 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { IEmbeddable, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { PluginSetup as SecuritySolutionPluginSetup } from '../../../../../plugins/security_solution/public'; export type ResolverTestPluginSetup = void; export type ResolverTestPluginStart = void; -export interface ResolverTestPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface -export interface ResolverTestPluginStartDependencies { - embeddable: EmbeddableStart; +export interface ResolverTestPluginSetupDependencies { + securitySolution: SecuritySolutionPluginSetup; } +export interface ResolverTestPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface export class ResolverTestPlugin implements @@ -23,34 +23,24 @@ export class ResolverTestPlugin ResolverTestPluginSetupDependencies, ResolverTestPluginStartDependencies > { - public setup(core: CoreSetup) { + public setup( + core: CoreSetup, + setupDependencies: ResolverTestPluginSetupDependencies + ) { core.application.register({ - id: 'resolver_test', - title: i18n.translate('xpack.resolver_test.pluginTitle', { + id: 'resolverTest', + title: i18n.translate('xpack.resolverTest.pluginTitle', { defaultMessage: 'Resolver Test', }), - mount: async (_context, params) => { - let resolveEmbeddable: ( - value: IEmbeddable | undefined | PromiseLike | undefined - ) => void; + mount: async (params: AppMountParameters) => { + const startServices = await core.getStartServices(); + const [coreStart] = startServices; - const promise = new Promise((resolve) => { - resolveEmbeddable = resolve; - }); - - (async () => { - const [, { embeddable }] = await core.getStartServices(); - const factory = embeddable.getEmbeddableFactory('resolver'); - if (factory) { - resolveEmbeddable!(factory.create({ id: 'test basic render' })); - } - })(); - - const { renderApp } = await import('./applications/resolver_test'); - /** - * Pass a promise which resolves to the Resolver embeddable. - */ - return renderApp(params, promise); + const [{ renderApp }, resolverPluginSetup] = await Promise.all([ + import('./applications/resolver_test'), + setupDependencies.securitySolution.resolver(), + ]); + return renderApp(coreStart, params, resolverPluginSetup); }, }); }
+ {message} ({statusCode}) +
- {' '} - - } - /> -
- - - - .{' '} - -
+ {' '} + + } + /> +
+ + + + .{' '} + +
+ + + +