From dc57163bf088f0b71e1ff2445744b70a0cbe36b3 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Fri, 30 Apr 2021 21:37:42 -0700 Subject: [PATCH] Provide guidance of "Custom" allocation behavior in ILM (#96111) * Provide allocation behavior guidance when the user has selected Custom, but no attributes are available and: ...data nodes are in use, so indices will allocate to data nodes. ...data tiers are available, so indices will allocate to data tiers. ...data tiers are available but not for the given phase's tier, so indices will be allocated to a fallback data tier. ...no data tiers are available, so allocation won't occur. * Provide link to the migration docs. * Adjust copy to consistently notify the user that node attributes are missing and guide them to use role-based allocation. * Localize disableDataTierOption flag instead of passing it as a prop, to make it easier to reason about. * Clarify the scenario in which the user has selected node_attrs allocation but no data tiers are available. * Remove inapplicable frozen tier from NoTiersAvailableNotice. * Refactor and update tests. * Split up node allocation tests to improve discoverability. Refactor test helpers to define suite-specific helper interfaces, to improve maintainability. * Refactor tests to make the [using node attributes, using node roles] branch and the various conditions within easier to contrast and compare. * Add coverage for NoTiersAvailableUsingNodeAttributesNotice scenario. * Add some helpers for improved maintainability and readability. * Create helpers/types file to store extracted Phase type. --- .../client_integration/app/app.helpers.tsx | 3 +- .../edit_policy/edit_policy.helpers.tsx | 233 ++------ .../features/node_allocation.test.ts | 503 ------------------ .../cloud_aware_behavior.helpers.ts | 37 ++ .../cloud_aware_behavior.test.ts | 154 ++++++ .../node_allocation/cold_phase.helpers.ts | 25 + .../node_allocation/cold_phase.test.ts | 226 ++++++++ .../general_behavior.helpers.ts | 26 + .../node_allocation/general_behavior.test.ts | 128 +++++ .../node_allocation/warm_phase.helpers.ts | 25 + .../node_allocation/warm_phase.test.ts | 214 ++++++++ .../edit_policy/init_test_bed.tsx | 55 ++ .../helpers/create_enable_phase_action.ts | 15 + .../helpers/create_form_set_value_action.ts | 23 + .../helpers/create_form_toggle_action.ts | 21 + .../helpers/create_node_allocation_actions.ts | 83 +++ .../helpers/global_mocks.tsx | 34 ++ .../client_integration/helpers/index.ts | 27 +- .../helpers/save_policy_action.ts | 19 + .../helpers/set_replicas_action.ts | 21 + .../client_integration/helpers/types.ts | 10 + .../common/types/policies.ts | 2 +- .../get_available_node_roles_for_phase.ts | 6 +- .../components/data_tier_allocation.tsx | 16 +- .../components/default_allocation_notice.tsx | 112 ---- .../components/default_allocation_warning.tsx | 72 --- .../default_to_data_nodes_notice.tsx | 38 ++ .../default_to_data_tiers_notice.tsx | 60 +++ .../components/index.ts | 12 +- .../no_custom_attributes_messages.tsx | 37 ++ .../components/no_node_attributes_warning.tsx | 60 --- .../components/no_tiers_available_notice.tsx | 49 ++ ...available_using_node_attributes_notice.tsx | 36 ++ .../components/node_allocation.tsx | 32 +- .../node_role_to_fallback_tier_map.ts | 19 + .../components/types.ts | 8 +- .../will_use_fallback_tier_notice.tsx | 49 ++ ...back_tier_using_node_attributes_notice.tsx | 47 ++ .../data_tier_allocation_field.tsx | 101 +++- .../application/services/documentation.ts | 2 + .../translations/translations/ja-JP.json | 13 - .../translations/translations/zh-CN.json | 13 - 42 files changed, 1642 insertions(+), 1024 deletions(-) delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.helpers.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_enable_phase_action.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_form_set_value_action.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_form_toggle_action.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_node_allocation_actions.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/global_mocks.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/save_policy_action.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/set_replicas_action.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_to_data_nodes_notice.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_to_data_tiers_notice.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_custom_attributes_messages.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_tiers_available_notice.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_tiers_available_using_node_attributes_notice.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_role_to_fallback_tier_map.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/will_use_fallback_tier_notice.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/will_use_fallback_tier_using_node_attributes_notice.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx index 808d8ad238c3e8..77b0372e249944 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx @@ -12,7 +12,6 @@ import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_reac import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; import { licensingMock } from '../../../../licensing/public/mocks'; import { App } from '../../../public/application/app'; -import { TestSubjects } from '../helpers'; const breadcrumbService = createBreadcrumbsMock(); @@ -37,7 +36,7 @@ const getTestBedConfig = (initialEntries: string[]): TestBedConfig => ({ const initTestBed = (initialEntries: string[]) => registerTestBed(AppWithContext, getTestBedConfig(initialEntries))(); -export interface AppTestBed extends TestBed { +export interface AppTestBed extends TestBed { actions: { clickPolicyNameLink: () => void; clickCreatePolicyButton: () => void; 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 26ed0a7728d2f0..67a978bb084714 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 @@ -5,88 +5,23 @@ * 2.0. */ -import React from 'react'; import { act } from 'react-dom/test-utils'; +import { TestBedConfig } from '@kbn/test/jest'; -import { registerTestBed, TestBedConfig } from '@kbn/test/jest'; - -import { licensingMock } from '../../../../licensing/public/mocks'; - -import { EditPolicy } from '../../../public/application/sections/edit_policy'; -import { DataTierAllocationType } from '../../../public/application/sections/edit_policy/types'; - -import { Phases as PolicyPhases } from '../../../common/types'; - -import { KibanaContextProvider } from '../../../public/shared_imports'; import { AppServicesContext } from '../../../public/types'; -import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; - -import { TestSubjects } from '../helpers'; -import { POLICY_NAME } from './constants'; - -type Phases = keyof PolicyPhases; - -window.scrollTo = jest.fn(); -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - - return { - ...original, - // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, - // which does not produce a valid component wrapper - EuiComboBox: (props: any) => ( - { - props.onChange([syntheticEvent['0']]); - }} - /> - ), - EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now. - }; -}); - -const getTestBedConfig = (testBedConfigArgs?: Partial): TestBedConfig => { - return { - memoryRouter: { - initialEntries: [`/policies/edit/${POLICY_NAME}`], - componentRoutePath: `/policies/edit/:policyName`, - }, - defaultProps: { - getUrlForApp: () => {}, - }, - ...testBedConfigArgs, - }; -}; - -const breadcrumbService = createBreadcrumbsMock(); - -const MyComponent = ({ appServicesContext, ...rest }: any) => { - return ( - - - - ); -}; - -const initTestBed = (arg?: { - appServicesContext?: Partial; - testBedConfig?: Partial; -}) => { - const { testBedConfig: testBedConfigArgs, ...rest } = arg || {}; - return registerTestBed(MyComponent, getTestBedConfig(testBedConfigArgs))(rest); -}; +import { + Phase, + createEnablePhaseAction, + createNodeAllocationActions, + createFormToggleAction, + createFormSetValueAction, + setReplicas, + savePolicy, +} from '../helpers'; +import { initTestBed } from './init_test_bed'; type SetupReturn = ReturnType; - export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; export const setup = async (arg?: { @@ -97,13 +32,6 @@ export const setup = async (arg?: { const { find, component, form, exists } = testBed; - const createFormToggleAction = (dataTestSubject: string) => async (checked: boolean) => { - await act(async () => { - form.toggleEuiSwitch(dataTestSubject, checked); - }); - component.update(); - }; - const createFormCheckboxAction = (dataTestSubject: string) => async (checked: boolean) => { await act(async () => { form.selectCheckBox(dataTestSubject, checked); @@ -111,15 +39,6 @@ export const setup = async (arg?: { component.update(); }; - function createFormSetValueAction(dataTestSubject: string) { - return async (value: V) => { - await act(async () => { - form.setInputValue(dataTestSubject, value); - }); - component.update(); - }; - } - const setWaitForSnapshotPolicy = async (snapshotPolicyName: string) => { act(() => { find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]); @@ -127,16 +46,9 @@ export const setup = async (arg?: { component.update(); }; - const savePolicy = async () => { - await act(async () => { - find('savePolicyButton').simulate('click'); - }); - component.update(); - }; - - const toggleDefaultRollover = createFormToggleAction('useDefaultRolloverSwitch'); + const toggleDefaultRollover = createFormToggleAction(testBed, 'useDefaultRolloverSwitch'); - const toggleRollover = createFormToggleAction('rolloverSwitch'); + const toggleRollover = createFormToggleAction(testBed, 'rolloverSwitch'); const setMaxPrimaryShardSize = async (value: string, units?: string) => { await act(async () => { @@ -162,7 +74,7 @@ export const setup = async (arg?: { component.update(); }; - const setMaxDocs = createFormSetValueAction('hot-selectedMaxDocuments'); + const setMaxDocs = createFormSetValueAction(testBed, 'hot-selectedMaxDocuments'); const setMaxAge = async (value: string, units?: string) => { await act(async () => { @@ -174,69 +86,64 @@ export const setup = async (arg?: { component.update(); }; - const createForceMergeActions = (phase: Phases) => { + const createForceMergeActions = (phase: Phase) => { const toggleSelector = `${phase}-forceMergeSwitch`; return { forceMergeFieldExists: () => exists(toggleSelector), - toggleForceMerge: createFormToggleAction(toggleSelector), - setForcemergeSegmentsCount: createFormSetValueAction(`${phase}-selectedForceMergeSegments`), + toggleForceMerge: createFormToggleAction(testBed, toggleSelector), + setForcemergeSegmentsCount: createFormSetValueAction( + testBed, + `${phase}-selectedForceMergeSegments` + ), setBestCompression: createFormCheckboxAction(`${phase}-bestCompression`), }; }; - const createIndexPriorityActions = (phase: Phases) => { + const createIndexPriorityActions = (phase: Phase) => { const toggleSelector = `${phase}-indexPrioritySwitch`; return { indexPriorityExists: () => exists(toggleSelector), - toggleIndexPriority: createFormToggleAction(toggleSelector), - setIndexPriority: createFormSetValueAction(`${phase}-indexPriority`), + toggleIndexPriority: createFormToggleAction(testBed, toggleSelector), + setIndexPriority: createFormSetValueAction(testBed, `${phase}-indexPriority`), }; }; - const enable = (phase: Phases) => createFormToggleAction(`enablePhaseSwitch-${phase}`); - - const createMinAgeActions = (phase: Phases) => { + const createMinAgeActions = (phase: Phase) => { return { hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`), - setMinAgeValue: createFormSetValueAction(`${phase}-selectedMinimumAge`), - setMinAgeUnits: createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`), + setMinAgeValue: createFormSetValueAction(testBed, `${phase}-selectedMinimumAge`), + setMinAgeUnits: createFormSetValueAction(testBed, `${phase}-selectedMinimumAgeUnits`), hasRolloverTipOnMinAge: () => exists(`${phase}-rolloverMinAgeInputIconTip`), }; }; - const setReplicas = (phase: Phases) => async (value: string) => { - if (!exists(`${phase}-selectedReplicaCount`)) { - await createFormToggleAction(`${phase}-setReplicasSwitch`)(true); - } - await createFormSetValueAction(`${phase}-selectedReplicaCount`)(value); - }; - - const createShrinkActions = (phase: Phases) => { + const createShrinkActions = (phase: Phase) => { const toggleSelector = `${phase}-shrinkSwitch`; return { shrinkExists: () => exists(toggleSelector), - toggleShrink: createFormToggleAction(toggleSelector), - setShrink: createFormSetValueAction(`${phase}-primaryShardCount`), + toggleShrink: createFormToggleAction(testBed, toggleSelector), + setShrink: createFormSetValueAction(testBed, `${phase}-primaryShardCount`), }; }; - const createSetFreeze = (phase: Phases) => createFormToggleAction(`${phase}-freezeSwitch`); - const createFreezeExists = (phase: Phases) => () => exists(`${phase}-freezeSwitch`); + const createSetFreeze = (phase: Phase) => + createFormToggleAction(testBed, `${phase}-freezeSwitch`); + const createFreezeExists = (phase: Phase) => () => exists(`${phase}-freezeSwitch`); - const createReadonlyActions = (phase: Phases) => { + const createReadonlyActions = (phase: Phase) => { const toggleSelector = `${phase}-readonlySwitch`; return { readonlyExists: () => exists(toggleSelector), - toggleReadonly: createFormToggleAction(toggleSelector), + toggleReadonly: createFormToggleAction(testBed, toggleSelector), }; }; - const createSearchableSnapshotActions = (phase: Phases) => { + const createSearchableSnapshotActions = (phase: Phase) => { const fieldSelector = `searchableSnapshotField-${phase}`; const licenseCalloutSelector = `${fieldSelector}.searchableSnapshotDisabledDueToLicense`; const toggleSelector = `${fieldSelector}.searchableSnapshotToggle`; - const toggleSearchableSnapshot = createFormToggleAction(toggleSelector); + const toggleSearchableSnapshot = createFormToggleAction(testBed, toggleSelector); return { searchableSnapshotDisabled: () => exists(licenseCalloutSelector) && find(licenseCalloutSelector).props().disabled === true, @@ -269,54 +176,6 @@ export const setup = async (arg?: { const hasRolloverSettingRequiredCallout = (): boolean => exists('rolloverSettingsRequired'); - const createNodeAllocationActions = (phase: Phases) => { - const controlsSelector = `${phase}-dataTierAllocationControls`; - const dataTierSelector = `${controlsSelector}.dataTierSelect`; - const nodeAttrsSelector = `${phase}-selectedNodeAttrs`; - - const openNodeAttributesSection = async () => { - await act(async () => { - find(dataTierSelector).simulate('click'); - }); - component.update(); - }; - - return { - hasDataTierAllocationControls: () => exists(controlsSelector), - openNodeAttributesSection, - hasNodeAttributesSelect: (): boolean => exists(nodeAttrsSelector), - getNodeAttributesSelectOptions: () => find(nodeAttrsSelector).find('option'), - setDataAllocation: async (value: DataTierAllocationType) => { - await openNodeAttributesSection(); - - await act(async () => { - switch (value) { - case 'node_roles': - find(`${controlsSelector}.defaultDataAllocationOption`).simulate('click'); - break; - case 'node_attrs': - find(`${controlsSelector}.customDataAllocationOption`).simulate('click'); - break; - default: - find(`${controlsSelector}.noneDataAllocationOption`).simulate('click'); - } - }); - component.update(); - }, - setSelectedNodeAttribute: createFormSetValueAction(nodeAttrsSelector), - hasNoNodeAttrsWarning: () => exists('noNodeAttributesWarning'), - hasDefaultAllocationWarning: () => exists('defaultAllocationWarning'), - hasDefaultAllocationNotice: () => exists('defaultAllocationNotice'), - hasNodeDetailsFlyout: () => exists(`${phase}-viewNodeDetailsFlyoutButton`), - openNodeDetailsFlyout: async () => { - await act(async () => { - find(`${phase}-viewNodeDetailsFlyoutButton`).simulate('click'); - }); - component.update(); - }, - }; - }; - const expectErrorMessages = (expectedMessages: string[]) => { const errorMessages = component.find('.euiFormErrorText'); expect(errorMessages.length).toBe(expectedMessages.length); @@ -346,10 +205,10 @@ export const setup = async (arg?: { ...testBed, runTimers, actions: { - saveAsNewPolicy: createFormToggleAction('saveAsNewSwitch'), - setPolicyName: createFormSetValueAction('policyNameField'), + saveAsNewPolicy: createFormToggleAction(testBed, 'saveAsNewSwitch'), + setPolicyName: createFormSetValueAction(testBed, 'policyNameField'), setWaitForSnapshotPolicy, - savePolicy, + savePolicy: () => savePolicy(testBed), hasGlobalErrorCallout: () => exists('policyFormErrorsCallout'), expectErrorMessages, timeline: { @@ -375,30 +234,30 @@ export const setup = async (arg?: { ...createSearchableSnapshotActions('hot'), }, warm: { - enable: enable('warm'), + enable: createEnablePhaseAction(testBed, 'warm'), ...createMinAgeActions('warm'), - setReplicas: setReplicas('warm'), + setReplicas: (value: string) => setReplicas(testBed, 'warm', value), hasErrorIndicator: () => exists('phaseErrorIndicator-warm'), ...createShrinkActions('warm'), ...createForceMergeActions('warm'), ...createReadonlyActions('warm'), ...createIndexPriorityActions('warm'), - ...createNodeAllocationActions('warm'), + ...createNodeAllocationActions(testBed, 'warm'), }, cold: { - enable: enable('cold'), + enable: createEnablePhaseAction(testBed, 'cold'), ...createMinAgeActions('cold'), - setReplicas: setReplicas('cold'), + setReplicas: (value: string) => setReplicas(testBed, 'cold', value), setFreeze: createSetFreeze('cold'), freezeExists: createFreezeExists('cold'), ...createReadonlyActions('cold'), hasErrorIndicator: () => exists('phaseErrorIndicator-cold'), ...createIndexPriorityActions('cold'), ...createSearchableSnapshotActions('cold'), - ...createNodeAllocationActions('cold'), + ...createNodeAllocationActions(testBed, 'cold'), }, frozen: { - enable: enable('frozen'), + enable: createEnablePhaseAction(testBed, 'frozen'), ...createMinAgeActions('frozen'), hasErrorIndicator: () => exists('phaseErrorIndicator-frozen'), ...createSearchableSnapshotActions('frozen'), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts deleted file mode 100644 index e289991780c04a..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts +++ /dev/null @@ -1,503 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { - POLICY_WITH_MIGRATE_OFF, - POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, - POLICY_WITH_NODE_ROLE_ALLOCATION, -} from '../constants'; - -describe(' node allocation', () => { - let testBed: EditPolicyTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - server.restore(); - }); - - beforeEach(async () => { - server.respondImmediately = true; - httpRequestsMockHelpers.setLoadPolicies([]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: { data: ['node1'] }, - nodesByAttributes: { 'attribute:true': ['node1'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - httpRequestsMockHelpers.setNodesDetails('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - describe('warm phase', () => { - test('shows spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - - const { actions, component } = testBed; - await actions.warm.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(actions.warm.hasDataTierAllocationControls()).toBeTruthy(); - - expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); - }); - - test('shows warning instead of node attributes input when none exist', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - - await act(async () => { - testBed = await setup(); - }); - const { actions, component } = testBed; - - component.update(); - await actions.warm.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - await actions.warm.setDataAllocation('node_attrs'); - expect(actions.warm.hasNoNodeAttrsWarning()).toBeTruthy(); - expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); - }); - - test('shows node attributes input when attributes exist', async () => { - const { actions, component } = testBed; - await actions.warm.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await actions.warm.setDataAllocation('node_attrs'); - expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); - expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); - expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); - }); - - test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { - const { actions, component } = testBed; - await actions.warm.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await actions.warm.setDataAllocation('node_attrs'); - expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); - expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); - - expect(actions.warm.hasNodeDetailsFlyout()).toBeFalsy(); - expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); - await actions.warm.setSelectedNodeAttribute('attribute:true'); - - await actions.warm.openNodeDetailsFlyout(); - expect(actions.warm.hasNodeDetailsFlyout()).toBeTruthy(); - }); - - test('shows default allocation warning when no node roles are found', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - - await act(async () => { - testBed = await setup(); - }); - const { actions, component } = testBed; - - component.update(); - await actions.warm.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(actions.warm.hasDefaultAllocationWarning()).toBeTruthy(); - }); - - test('when configuring warm phase shows default allocation notice when hot tier exists, but not warm tier', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, - nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - - await act(async () => { - testBed = await setup(); - }); - const { actions, component } = testBed; - - component.update(); - await actions.warm.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(actions.warm.hasDefaultAllocationNotice()).toBeTruthy(); - }); - - test(`doesn't show default allocation notice when node with "data" role exists`, async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - await act(async () => { - testBed = await setup(); - }); - const { actions, component } = testBed; - - component.update(); - await actions.warm.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(actions.warm.hasDefaultAllocationNotice()).toBeFalsy(); - }); - }); - - describe('cold phase', () => { - test('shows spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - - const { actions, component } = testBed; - await actions.cold.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(actions.cold.hasDataTierAllocationControls()).toBeTruthy(); - - expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); - }); - - test('shows warning instead of node attributes input when none exist', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - - await act(async () => { - testBed = await setup(); - }); - const { actions, component } = testBed; - - component.update(); - await actions.cold.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await actions.cold.setDataAllocation('node_attrs'); - expect(actions.cold.hasNoNodeAttrsWarning()).toBeTruthy(); - expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); - }); - - test('shows node attributes input when attributes exist', async () => { - const { actions, component } = testBed; - await actions.cold.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await actions.cold.setDataAllocation('node_attrs'); - expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); - expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); - expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); - }); - - test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { - const { actions, component } = testBed; - - await actions.cold.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await actions.cold.setDataAllocation('node_attrs'); - expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); - expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); - - expect(actions.cold.hasNodeDetailsFlyout()).toBeFalsy(); - expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); - await actions.cold.setSelectedNodeAttribute('attribute:true'); - - await actions.cold.openNodeDetailsFlyout(); - expect(actions.cold.hasNodeDetailsFlyout()).toBeTruthy(); - }); - - test('shows default allocation warning when no node roles are found', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - - await act(async () => { - testBed = await setup(); - }); - const { actions, component } = testBed; - - component.update(); - await actions.cold.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(actions.cold.hasDefaultAllocationWarning()).toBeTruthy(); - }); - - [ - { - nodesByRoles: { data_hot: ['test'] }, - previousActiveRole: 'hot', - }, - { - nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, - previousActiveRole: 'warm', - }, - ].forEach(({ nodesByRoles, previousActiveRole }) => { - test(`shows default allocation notice when ${previousActiveRole} tiers exists, but not cold tier`, async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, - nodesByRoles, - isUsingDeprecatedDataRoleConfig: false, - }); - - await act(async () => { - testBed = await setup(); - }); - const { actions, component, find } = testBed; - - component.update(); - await actions.cold.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy(); - expect(find('defaultAllocationNotice').text()).toContain( - `This policy will move data in the cold phase to ${previousActiveRole} tier nodes` - ); - }); - }); - - test(`doesn't show default allocation notice when node with "data" role exists`, async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - await act(async () => { - testBed = await setup(); - }); - const { actions, component } = testBed; - - component.update(); - await actions.cold.enable(true); - - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); - }); - }); - - describe('not on cloud', () => { - test('shows all allocation options, even if using legacy config', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - await act(async () => { - testBed = await setup(); - }); - const { actions, component, exists } = testBed; - - component.update(); - await actions.warm.enable(true); - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that default, custom and 'none' options exist - await actions.warm.openNodeAttributesSection(); - expect(exists('defaultDataAllocationOption')).toBeTruthy(); - expect(exists('customDataAllocationOption')).toBeTruthy(); - expect(exists('noneDataAllocationOption')).toBeTruthy(); - }); - }); - - describe('on cloud', () => { - describe('using legacy data role config', () => { - test('should hide data tier option on cloud', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: { test: ['123'] }, - // On cloud, if using legacy config there will not be any "data_*" roles set. - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); - }); - const { actions, component, exists, find } = testBed; - - component.update(); - await actions.warm.enable(true); - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that custom and 'none' options exist - await actions.warm.openNodeAttributesSection(); - expect(exists('defaultDataAllocationOption')).toBeFalsy(); - expect(exists('customDataAllocationOption')).toBeTruthy(); - expect(exists('noneDataAllocationOption')).toBeTruthy(); - // Show the call-to-action for users to migrate their cluster to use node roles - expect(find('cloudDataTierCallout').exists()).toBeTruthy(); - }); - }); - - describe('using node role config', () => { - test('shows recommended, custom and "off" options on cloud with data roles', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); - }); - testBed.component.update(); - - const { actions, component, exists, find } = testBed; - await actions.warm.enable(true); - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - await actions.warm.openNodeAttributesSection(); - expect(exists('defaultDataAllocationOption')).toBeTruthy(); - expect(exists('customDataAllocationOption')).toBeTruthy(); - expect(exists('noneDataAllocationOption')).toBeTruthy(); - // Do not show the call-to-action for users to migrate their cluster to use node roles - expect(find('cloudDataTierCallout').exists()).toBeFalsy(); - }); - test('do not show node allocation specific warnings on cloud', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: { test: ['123'] }, - // No nodes with node roles like "data_hot" or "data_warm" - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); - }); - testBed.component.update(); - - const { actions, component, exists } = testBed; - await actions.warm.enable(true); - await actions.cold.enable(true); - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - expect(exists('cloudDataTierCallout')).toBeFalsy(); - expect(exists('defaultAllocationNotice')).toBeFalsy(); - expect(exists('defaultAllocationWarning')).toBeFalsy(); - }); - }); - }); - - describe('data allocation', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_MIGRATE_OFF]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: {}, - nodesByAttributes: { test: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('setting node_attr based allocation, but not selecting node attribute', async () => { - const { actions } = testBed; - await actions.warm.setDataAllocation('node_attrs'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm; - - expect(warmPhase.actions.migrate).toEqual({ enabled: false }); - }); - - describe('node roles', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ROLE_ALLOCATION]); - httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, - }); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('detecting use of the recommended allocation type', () => { - const { find } = testBed; - const selectedDataAllocation = find( - 'warm-dataTierAllocationControls.dataTierSelect' - ).text(); - expect(selectedDataAllocation).toBe('Use warm nodes (recommended)'); - }); - - test('setting replicas serialization', async () => { - const { actions } = testBed; - await actions.warm.setReplicas('123'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const warmPhaseActions = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm - .actions; - expect(warmPhaseActions).toMatchInlineSnapshot(` - Object { - "allocate": Object { - "number_of_replicas": 123, - }, - } - `); - }); - }); - - describe('node attr and none', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION]); - httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, - }); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('detecting use of the custom allocation type', () => { - const { find } = testBed; - expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom'); - }); - - test('detecting use of the "off" allocation type', () => { - const { find } = testBed; - expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); - }); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts new file mode 100644 index 00000000000000..ba376f9cddd938 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TestBedConfig } from '@kbn/test/jest'; + +import { AppServicesContext } from '../../../../../public/types'; +import { createEnablePhaseAction, createNodeAllocationActions } from '../../../helpers'; +import { initTestBed } from '../../init_test_bed'; + +type SetupReturn = ReturnType; + +export type CloudNodeAllocationTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupCloudNodeAllocation = async (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { + const testBed = await initTestBed(arg); + + return { + ...testBed, + actions: { + warm: { + enable: createEnablePhaseAction(testBed, 'warm'), + ...createNodeAllocationActions(testBed, 'warm'), + }, + cold: { + enable: createEnablePhaseAction(testBed, 'cold'), + ...createNodeAllocationActions(testBed, 'cold'), + }, + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts new file mode 100644 index 00000000000000..06b667f6668080 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../../helpers/setup_environment'; +import { + CloudNodeAllocationTestBed, + setupCloudNodeAllocation, +} from './cloud_aware_behavior.helpers'; + +describe(' node allocation cloud-aware behavior', () => { + let testBed: CloudNodeAllocationTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + const setup = async (isOnCloud?: boolean) => { + await act(async () => { + if (Boolean(isOnCloud)) { + testBed = await setupCloudNodeAllocation({ + appServicesContext: { cloud: { isCloudEnabled: true } }, + }); + } else { + testBed = await setupCloudNodeAllocation(); + } + }); + }; + + beforeEach(async () => { + server.respondImmediately = true; + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await setup(); + + const { component } = testBed; + component.update(); + }); + + describe('when not on cloud', () => { + test('shows all allocation options, even if using legacy config', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + + await setup(); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that default, custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + }); + }); + + describe('when on cloud', () => { + describe('using legacy data role config', () => { + test('should hide data tier option on cloud', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + // On cloud, if using legacy config there will not be any "data_*" roles set. + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await setup(true); + const { actions, component, exists, find } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeFalsy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + // Show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeTruthy(); + }); + }); + + describe('using node role config', () => { + test('shows recommended, custom and "off" options on cloud with data roles', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await setup(true); + testBed.component.update(); + + const { actions, component, exists, find } = testBed; + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + // Do not show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeFalsy(); + }); + + test('do not show node allocation specific warnings on cloud', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + // No nodes with node roles like "data_hot" or "data_warm" + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + await setup(true); + testBed.component.update(); + + const { actions, component, exists } = testBed; + await actions.warm.enable(true); + await actions.cold.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + expect(exists('cloudDataTierCallout')).toBeFalsy(); + expect( + actions.warm.hasWillUseFallbackTierNotice() || actions.cold.hasWillUseFallbackTierNotice() + ).toBeFalsy(); + expect( + actions.warm.hasNoTiersAvailableNotice() || actions.cold.hasNoTiersAvailableNotice() + ).toBeFalsy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.helpers.ts new file mode 100644 index 00000000000000..52593e67df7689 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.helpers.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createEnablePhaseAction, createNodeAllocationActions } from '../../../helpers'; +import { initTestBed } from '../../init_test_bed'; + +type SetupReturn = ReturnType; + +export type NodeAllocationTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupColdPhaseNodeAllocation = async () => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: { + enable: createEnablePhaseAction(testBed, 'cold'), + ...createNodeAllocationActions(testBed, 'cold'), + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts new file mode 100644 index 00000000000000..f833c8e3161174 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../../helpers/setup_environment'; +import { NodeAllocationTestBed, setupColdPhaseNodeAllocation } from './cold_phase.helpers'; + +describe(' node allocation in the cold phase', () => { + let testBed: NodeAllocationTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + const setup = async () => { + await act(async () => { + testBed = await setupColdPhaseNodeAllocation(); + }); + }; + + beforeEach(async () => { + server.respondImmediately = true; + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await setup(); + + const { component } = testBed; + component.update(); + }); + + test(`doesn't offer allocation guidance when node with deprecated "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await setup(); + const { actions, component } = testBed; + + component.update(); + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + expect(actions.hasWillUseFallbackTierNotice()).toBeFalsy(); + }); + + describe('when using node attributes', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions } = testBed; + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeTruthy(); + expect(actions.hasDataTierAllocationControls()).toBeTruthy(); + expect(actions.hasNodeAttributesSelect()).toBeFalsy(); + + // No notices will be shown. + expect(actions.hasDefaultToDataTiersNotice()).toBeFalsy(); + expect(actions.hasWillUseFallbackTierUsingNodeAttributesNotice()).toBeFalsy(); + expect(actions.hasDefaultToDataNodesNotice()).toBeFalsy(); + expect(actions.hasNoTiersAvailableUsingNodeAttributesNotice()).toBeFalsy(); + }); + + describe('and some are defined', () => { + test('shows the node attributes input', async () => { + const { actions } = testBed; + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + await actions.setDataAllocation('node_attrs'); + expect(actions.hasDefaultAllocationBehaviorNotice()).toBeFalsy(); + expect(actions.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when an attribute is selected and shows flyout when clicked', async () => { + const { actions } = testBed; + + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + await actions.setDataAllocation('node_attrs'); + expect(actions.hasDefaultAllocationBehaviorNotice()).toBeFalsy(); + expect(actions.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.getNodeAttributesSelectOptions().length).toBe(2); + await actions.setSelectedNodeAttribute('attribute:true'); + + await actions.openNodeDetailsFlyout(); + expect(actions.hasNodeDetailsFlyout()).toBeTruthy(); + }); + }); + + describe('and none are defined', () => { + const commonSetupAndBaselineAssertions = async () => { + await setup(); + const { actions, component } = testBed; + component.update(); + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + await actions.setDataAllocation('node_attrs'); + expect(actions.hasNodeAttributesSelect()).toBeFalsy(); + }; + + test('and data tiers are available, shows DefaultToDataTiersNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await commonSetupAndBaselineAssertions(); + const { actions } = testBed; + expect(actions.hasDefaultToDataTiersNotice()).toBeTruthy(); + }); + + test('and data tiers are available, but not for the cold phase, shows WillUseFallbackTierUsingNodeAttributesNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_warm: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await commonSetupAndBaselineAssertions(); + const { actions } = testBed; + expect(actions.hasWillUseFallbackTierUsingNodeAttributesNotice()).toBeTruthy(); + }); + + test('when data nodes are in use, shows DefaultToDataNodesNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: true, + }); + + await commonSetupAndBaselineAssertions(); + const { actions } = testBed; + expect(actions.hasDefaultToDataNodesNotice()).toBeTruthy(); + }); + + test('when no data tier node roles are defined, shows NoTiersAvailableUsingNodeAttributesNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + // data_content is a node role so they're technically in use, but it's not a data tier node role. + nodesByRoles: { data_content: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await commonSetupAndBaselineAssertions(); + const { actions } = testBed; + expect(actions.hasNoTiersAvailableUsingNodeAttributesNotice()).toBeTruthy(); + }); + }); + }); + + describe('when using node roles', () => { + test('when no node roles are defined, shows NoTiersAvailableNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await setup(); + const { actions, component } = testBed; + + component.update(); + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + expect(actions.hasNoTiersAvailableNotice()).toBeTruthy(); + }); + + [ + { + nodesByRoles: { data_hot: ['test'] }, + fallbackTier: 'hot', + }, + { + nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, + fallbackTier: 'warm', + }, + ].forEach(({ nodesByRoles, fallbackTier }) => { + test(`when allocation will fallback to the ${fallbackTier} tier, shows WillUseFallbackTierNotice and defines the fallback tier`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles, + isUsingDeprecatedDataRoleConfig: false, + }); + + await setup(); + const { actions, component } = testBed; + + component.update(); + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + expect(actions.hasWillUseFallbackTierNotice()).toBeTruthy(); + expect(actions.getWillUseFallbackTierNoticeText()).toContain( + `No nodes assigned to the cold tierIf no cold nodes are available, data is stored in the ${fallbackTier} tier.` + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts new file mode 100644 index 00000000000000..8dd5a75ac0d220 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createNodeAllocationActions, savePolicy, setReplicas } from '../../../helpers'; +import { initTestBed } from '../../init_test_bed'; + +type SetupReturn = ReturnType; + +export type GeneralNodeAllocationTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupGeneralNodeAllocation = async () => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: { + ...createNodeAllocationActions(testBed, 'warm'), + savePolicy: () => savePolicy(testBed), + setReplicas: (value: string) => setReplicas(testBed, 'warm', value), + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts new file mode 100644 index 00000000000000..d45e1cfe9567b8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../../helpers/setup_environment'; +import { + GeneralNodeAllocationTestBed, + setupGeneralNodeAllocation, +} from './general_behavior.helpers'; +import { + POLICY_WITH_MIGRATE_OFF, + POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, + POLICY_WITH_NODE_ROLE_ALLOCATION, +} from '../../constants'; + +describe(' node allocation general behavior', () => { + let testBed: GeneralNodeAllocationTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + const setup = async () => { + await act(async () => { + testBed = await setupGeneralNodeAllocation(); + }); + }; + + describe('data allocation', () => { + test('setting node_attr based allocation, but not selecting node attribute', async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_MIGRATE_OFF]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await setup(); + + const { component, actions } = testBed; + component.update(); + + await actions.setDataAllocation('node_attrs'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm; + + expect(warmPhase.actions.migrate).toEqual({ enabled: false }); + }); + + describe('node roles', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ROLE_ALLOCATION]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + + await setup(); + + const { component } = testBed; + component.update(); + }); + + test('detecting use of the recommended allocation type', () => { + const { find } = testBed; + const selectedDataAllocation = find( + 'warm-dataTierAllocationControls.dataTierSelect' + ).text(); + expect(selectedDataAllocation).toBe('Use warm nodes (recommended)'); + }); + + test('setting replicas serialization', async () => { + const { actions } = testBed; + await actions.setReplicas('123'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhaseActions = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm + .actions; + expect(warmPhaseActions).toMatchInlineSnapshot(` + Object { + "allocate": Object { + "number_of_replicas": 123, + }, + } + `); + }); + }); + + describe('node attr and none', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + + await setup(); + + const { component } = testBed; + component.update(); + }); + + test('detecting use of the custom allocation type', () => { + const { find } = testBed; + expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom'); + }); + + test('detecting use of the "off" allocation type', () => { + const { find } = testBed; + expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.helpers.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.helpers.ts new file mode 100644 index 00000000000000..e39629da79cd89 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.helpers.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createNodeAllocationActions, createEnablePhaseAction } from '../../../helpers'; +import { initTestBed } from '../../init_test_bed'; + +type SetupReturn = ReturnType; + +export type NodeAllocationTestBed = SetupReturn extends Promise ? U : SetupReturn; + +export const setupWarmPhaseNodeAllocation = async () => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: { + enable: createEnablePhaseAction(testBed, 'warm'), + ...createNodeAllocationActions(testBed, 'warm'), + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts new file mode 100644 index 00000000000000..0ab18c6e05736c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../../helpers/setup_environment'; +import { NodeAllocationTestBed, setupWarmPhaseNodeAllocation } from './warm_phase.helpers'; + +describe(' node allocation in the warm phase', () => { + let testBed: NodeAllocationTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + const setup = async () => { + await act(async () => { + testBed = await setupWarmPhaseNodeAllocation(); + }); + }; + + beforeEach(async () => { + server.respondImmediately = true; + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await setup(); + + const { component } = testBed; + component.update(); + }); + + test(`doesn't offer allocation guidance when node with deprecated "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await setup(); + const { actions, component } = testBed; + + component.update(); + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + expect(actions.hasDefaultAllocationBehaviorNotice()).toBeFalsy(); + }); + + describe('when using node attributes', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions } = testBed; + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeTruthy(); + expect(actions.hasDataTierAllocationControls()).toBeTruthy(); + expect(actions.hasNodeAttributesSelect()).toBeFalsy(); + + // No notices will be shown. + expect(actions.hasDefaultToDataTiersNotice()).toBeFalsy(); + expect(actions.hasWillUseFallbackTierUsingNodeAttributesNotice()).toBeFalsy(); + expect(actions.hasDefaultToDataNodesNotice()).toBeFalsy(); + expect(actions.hasNoTiersAvailableUsingNodeAttributesNotice()).toBeFalsy(); + }); + + describe('and some are defined', () => { + test('shows the node attributes input', async () => { + const { actions } = testBed; + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + await actions.setDataAllocation('node_attrs'); + expect(actions.hasDefaultAllocationBehaviorNotice()).toBeFalsy(); + expect(actions.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when an attribute is selected and shows flyout when clicked', async () => { + const { actions } = testBed; + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + await actions.setDataAllocation('node_attrs'); + expect(actions.hasDefaultAllocationBehaviorNotice()).toBeFalsy(); + expect(actions.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.getNodeAttributesSelectOptions().length).toBe(2); + await actions.setSelectedNodeAttribute('attribute:true'); + + await actions.openNodeDetailsFlyout(); + expect(actions.hasNodeDetailsFlyout()).toBeTruthy(); + }); + }); + + describe('and none are defined', () => { + const commonSetupAndBaselineAssertions = async () => { + await setup(); + const { actions, component } = testBed; + component.update(); + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + await actions.setDataAllocation('node_attrs'); + expect(actions.hasNodeAttributesSelect()).toBeFalsy(); + }; + + test('and data tiers are available, shows DefaultToDataTiersNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await commonSetupAndBaselineAssertions(); + const { actions } = testBed; + expect(actions.hasDefaultToDataTiersNotice()).toBeTruthy(); + }); + + test('and data tiers are available, but not for the warm phase, shows WillUseFallbackTierUsingNodeAttributesNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await commonSetupAndBaselineAssertions(); + const { actions } = testBed; + expect(actions.hasWillUseFallbackTierUsingNodeAttributesNotice()).toBeTruthy(); + }); + + test('when data nodes are in use, shows DefaultToDataNodesNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: true, + }); + + await commonSetupAndBaselineAssertions(); + const { actions } = testBed; + expect(actions.hasDefaultToDataNodesNotice()).toBeTruthy(); + }); + + test('when no data tier node roles are defined, shows NoTiersAvailableUsingNodeAttributesNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + // data_content is a node role so they're technically in use, but it's not a data tier node role. + nodesByRoles: { data_content: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await commonSetupAndBaselineAssertions(); + const { actions } = testBed; + expect(actions.hasNoTiersAvailableUsingNodeAttributesNotice()).toBeTruthy(); + }); + }); + }); + + describe('when using node roles', () => { + test('when no node roles are defined, shows NoTiersAvailableNotice', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await setup(); + const { actions, component } = testBed; + + component.update(); + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + expect(actions.hasNoTiersAvailableNotice()).toBeTruthy(); + }); + + test('when allocation will fallback to the hot tier, shows WillUseFallbackTierNotice and defines the fallback tier', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await setup(); + const { actions, component } = testBed; + + component.update(); + await actions.enable(true); + + expect(actions.isAllocationLoading()).toBeFalsy(); + expect(actions.hasWillUseFallbackTierNotice()).toBeTruthy(); + expect(actions.getWillUseFallbackTierNoticeText()).toContain( + `No nodes assigned to the warm tierIf no warm nodes are available, data is stored in the hot tier.` + ); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.tsx new file mode 100644 index 00000000000000..4f057e04c85d4a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { registerTestBed, TestBedConfig } from '@kbn/test/jest'; + +import '../helpers/global_mocks'; + +import { licensingMock } from '../../../../licensing/public/mocks'; +import { EditPolicy } from '../../../public/application/sections/edit_policy'; +import { KibanaContextProvider } from '../../../public/shared_imports'; +import { AppServicesContext } from '../../../public/types'; +import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; +import { POLICY_NAME } from './constants'; + +const getTestBedConfig = (testBedConfigArgs?: Partial): TestBedConfig => { + return { + memoryRouter: { + initialEntries: [`/policies/edit/${POLICY_NAME}`], + componentRoutePath: `/policies/edit/:policyName`, + }, + defaultProps: { + getUrlForApp: () => {}, + }, + ...testBedConfigArgs, + }; +}; + +const breadcrumbService = createBreadcrumbsMock(); + +const EditPolicyContainer = ({ appServicesContext, ...rest }: any) => { + return ( + + + + ); +}; + +export const initTestBed = (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { + const { testBedConfig: testBedConfigArgs, ...rest } = arg || {}; + return registerTestBed(EditPolicyContainer, getTestBedConfig(testBedConfigArgs))(rest); +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_enable_phase_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_enable_phase_action.ts new file mode 100644 index 00000000000000..e0988daf52192e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_enable_phase_action.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TestBed } from '@kbn/test/jest'; + +import { Phase } from './types'; +import { createFormToggleAction } from './create_form_toggle_action'; + +export const createEnablePhaseAction = (testBed: TestBed, phase: Phase) => { + return createFormToggleAction(testBed, `enablePhaseSwitch-${phase}`); +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_form_set_value_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_form_set_value_action.ts new file mode 100644 index 00000000000000..95e90ac04192bd --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_form_set_value_action.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { TestBed } from '@kbn/test/jest'; + +export function createFormSetValueAction( + testBed: TestBed, + dataTestSubject: string +) { + const { form, component } = testBed; + + return async (value: V) => { + await act(async () => { + form.setInputValue(dataTestSubject, value); + }); + component.update(); + }; +} diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_form_toggle_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_form_toggle_action.ts new file mode 100644 index 00000000000000..579bb661871da1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_form_toggle_action.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { TestBed } from '@kbn/test/jest'; + +export const createFormToggleAction = (testBed: TestBed, dataTestSubject: string) => async ( + checked: boolean +) => { + const { form, component } = testBed; + + await act(async () => { + form.toggleEuiSwitch(dataTestSubject, checked); + }); + + component.update(); +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_node_allocation_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_node_allocation_actions.ts new file mode 100644 index 00000000000000..416a2afa54d54d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/create_node_allocation_actions.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { TestBed } from '@kbn/test/jest'; + +import { Phase } from './types'; +import { DataTierAllocationType } from '../../../public/application/sections/edit_policy/types'; +import { createFormSetValueAction } from './create_form_set_value_action'; + +export const createNodeAllocationActions = (testBed: TestBed, phase: Phase) => { + const { component, find, exists } = testBed; + + const controlsSelector = `${phase}-dataTierAllocationControls`; + const dataTierSelector = `${controlsSelector}.dataTierSelect`; + const nodeAttrsSelector = `${phase}-selectedNodeAttrs`; + + const openNodeAttributesSection = async () => { + await act(async () => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + }; + + const isAllocationLoading = () => find(`${phase}-phase.allocationLoadingSpinner`).exists(); + const hasDefaultToDataNodesNotice = () => exists(`${phase}-phase.defaultToDataNodesNotice`); + const hasDefaultToDataTiersNotice = () => exists(`${phase}-phase.defaultToDataTiersNotice`); + const hasDefaultAllocationBehaviorNotice = () => + hasDefaultToDataNodesNotice() && hasDefaultToDataTiersNotice(); + const hasNoTiersAvailableNotice = () => exists(`${phase}-phase.noTiersAvailableNotice`); + const hasNoTiersAvailableUsingNodeAttributesNotice = () => + exists(`${phase}-phase.noTiersAvailableUsingNodeAttributesNotice`); + const hasWillUseFallbackTierNotice = () => exists(`${phase}-phase.willUseFallbackTierNotice`); + const hasWillUseFallbackTierUsingNodeAttributesNotice = () => + exists(`${phase}-phase.willUseFallbackTierUsingNodeAttributesNotice`); + const getWillUseFallbackTierNoticeText = () => + find(`${phase}-phase.willUseFallbackTierNotice`).text(); + + return { + hasDataTierAllocationControls: () => exists(controlsSelector), + openNodeAttributesSection, + hasNodeAttributesSelect: (): boolean => exists(nodeAttrsSelector), + getNodeAttributesSelectOptions: () => find(nodeAttrsSelector).find('option'), + setDataAllocation: async (value: DataTierAllocationType) => { + await openNodeAttributesSection(); + + await act(async () => { + switch (value) { + case 'node_roles': + find(`${controlsSelector}.defaultDataAllocationOption`).simulate('click'); + break; + case 'node_attrs': + find(`${controlsSelector}.customDataAllocationOption`).simulate('click'); + break; + default: + find(`${controlsSelector}.noneDataAllocationOption`).simulate('click'); + } + }); + component.update(); + }, + setSelectedNodeAttribute: createFormSetValueAction(testBed, nodeAttrsSelector), + isAllocationLoading, + hasDefaultToDataNodesNotice, + hasDefaultToDataTiersNotice, + hasDefaultAllocationBehaviorNotice, + hasNoTiersAvailableNotice, + hasNoTiersAvailableUsingNodeAttributesNotice, + hasWillUseFallbackTierNotice, + hasWillUseFallbackTierUsingNodeAttributesNotice, + getWillUseFallbackTierNoticeText, + hasNodeDetailsFlyout: () => exists(`${phase}-viewNodeDetailsFlyoutButton`), + openNodeDetailsFlyout: async () => { + await act(async () => { + find(`${phase}-viewNodeDetailsFlyoutButton`).simulate('click'); + }); + component.update(); + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/global_mocks.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/global_mocks.tsx new file mode 100644 index 00000000000000..5ed5e6e8fcce77 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/global_mocks.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +// NOTE: Import this file for its side-effects. You must import it before the code that it mocks +// is imported. Typically this means just importing above your other imports. +// See https://jestjs.io/docs/manual-mocks for more info. + +window.scrollTo = jest.fn(); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now. + }; +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index 56f5815633a1db..1388cf97d4e223 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -5,22 +5,11 @@ * 2.0. */ -export type TestSubjects = - | 'snapshotPolicyCombobox' - | 'savePolicyButton' - | 'customPolicyCallout' - | 'noPoliciesCallout' - | 'policiesErrorCallout' - | 'rolloverSwitch' - | 'rolloverSettingsRequired' - | 'hot-selectedMaxSizeStored' - | 'hot-selectedMaxSizeStoredUnits' - | 'hot-selectedMaxDocuments' - | 'hot-selectedMaxAge' - | 'hot-selectedMaxAgeUnits' - | 'policyTablePolicyNameLink' - | 'policyTitle' - | 'createPolicyButton' - | 'cold-freezeSwitch' - | 'frozen-freezeSwitch' - | string; +export { Phase } from './types'; + +export { createNodeAllocationActions } from './create_node_allocation_actions'; +export { createEnablePhaseAction } from './create_enable_phase_action'; +export { setReplicas } from './set_replicas_action'; +export { savePolicy } from './save_policy_action'; +export { createFormToggleAction } from './create_form_toggle_action'; +export { createFormSetValueAction } from './create_form_set_value_action'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/save_policy_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/save_policy_action.ts new file mode 100644 index 00000000000000..37ce4f84f03ba3 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/save_policy_action.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { TestBed } from '@kbn/test/jest'; + +export const savePolicy = async (testBed: TestBed) => { + const { find, component } = testBed; + + await act(async () => { + find('savePolicyButton').simulate('click'); + }); + + component.update(); +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/set_replicas_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/set_replicas_action.ts new file mode 100644 index 00000000000000..049c1a205cbd4b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/set_replicas_action.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TestBed } from '@kbn/test/jest'; + +import { Phase } from './types'; +import { createFormToggleAction } from './create_form_toggle_action'; +import { createFormSetValueAction } from './create_form_set_value_action'; + +export const setReplicas = async (testBed: TestBed, phase: Phase, value: string) => { + const { exists } = testBed; + + if (!exists(`${phase}-selectedReplicaCount`)) { + await createFormToggleAction(testBed, `${phase}-setReplicasSwitch`)(true); + } + await createFormSetValueAction(testBed, `${phase}-selectedReplicaCount`)(value); +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts new file mode 100644 index 00000000000000..644ada96a9f051 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Phases } from '../../../common/types'; + +export type Phase = keyof Phases; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 337a1be2b2fbc8..385751f31d5bc0 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -7,7 +7,7 @@ import { Index as IndexInterface } from '../../../index_management/common/types'; -export type PhaseWithAllocation = 'warm' | 'cold' | 'frozen'; +export type PhaseWithAllocation = 'warm' | 'cold'; export interface SerializedPolicy { name: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/get_available_node_roles_for_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/get_available_node_roles_for_phase.ts index f2596cac8b9203..d5ba7fb7850695 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/get_available_node_roles_for_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/get_available_node_roles_for_phase.ts @@ -13,8 +13,6 @@ import { import { phaseToNodePreferenceMap } from '../../../../common/constants'; -export type AllocationNodeRole = DataTierRole | 'none'; - /** * Given a phase and current cluster node roles, determine which nodes the phase * will allocate data to. For instance, for the warm phase, with warm @@ -26,7 +24,7 @@ export type AllocationNodeRole = DataTierRole | 'none'; export const getAvailableNodeRoleForPhase = ( phase: PhaseWithAllocation, nodesByRoles: ListNodesRouteResponse['nodesByRoles'] -): AllocationNodeRole => { +): DataTierRole | undefined => { const preferredNodeRoles = phaseToNodePreferenceMap[phase]; // The 'data' role covers all node roles, so if we have at least one node with the data role @@ -35,5 +33,5 @@ export const getAvailableNodeRoleForPhase = ( return preferredNodeRoles[0]; } - return preferredNodeRoles.find((role) => Boolean(nodesByRoles[role]?.length)) ?? 'none'; + return preferredNodeRoles.find((role) => Boolean(nodesByRoles[role]?.length)); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx index 254063ac1a9ea4..1eeb32d84f677e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx @@ -161,7 +161,20 @@ const getSelectOptions = (phase: PhaseWithAllocation, disableDataTierOption: boo ].filter(Boolean) as SelectOptions[]; export const DataTierAllocation: FunctionComponent = (props) => { - const { phase, hasNodeAttributes, disableDataTierOption, isLoading } = props; + const { + phase, + hasNodeAttributes, + isCloudEnabled, + isUsingDeprecatedDataRoleConfig, + isLoading, + } = props; + + /** + * On Cloud we want to disable the data tier allocation option when we detect that we are not + * using node roles in our Node config yet. See {@link ListNodesRouteResponse} for information about how this is + * detected. + */ + const disableDataTierOption = Boolean(isCloudEnabled && isUsingDeprecatedDataRoleConfig); const dataTierAllocationTypePath = `_meta.${phase}.dataTierAllocationType`; @@ -185,6 +198,7 @@ export const DataTierAllocation: FunctionComponent = (props) => { if (disableDataTierOption && field.value === 'node_roles') { field.setValue('node_attrs'); } + return ( = { - data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', { - defaultMessage: 'hot', - }), - data_warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel', { - defaultMessage: 'warm', - }), - data_cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel', { - defaultMessage: 'cold', - }), - data_frozen: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierFrozenLabel', { - defaultMessage: 'frozen', - }), -}; - -const i18nTexts = { - notice: { - warm: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title', - { defaultMessage: 'No nodes assigned to the warm tier' } - ), - body: (nodeRole: DataTierRole) => - i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm', { - defaultMessage: - 'This policy will move data in the warm phase to {tier} tier nodes instead.', - values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] }, - }), - }, - cold: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold.title', - { defaultMessage: 'No nodes assigned to the cold tier' } - ), - body: (nodeRole: DataTierRole) => - i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold', { - defaultMessage: - 'This policy will move data in the cold phase to {tier} tier nodes instead.', - values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] }, - }), - }, - frozen: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.frozen.title', - { defaultMessage: 'No nodes assigned to the frozen tier' } - ), - body: (nodeRole: DataTierRole) => - i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.frozen', - { - defaultMessage: - 'This policy will move data in the frozen phase to {tier} tier nodes instead.', - values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] }, - } - ), - }, - }, - warning: { - warm: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle', - { defaultMessage: 'No nodes assigned to the warm tier' } - ), - body: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody', - { - defaultMessage: - 'Assign at least one node to the warm or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', - } - ), - }, - cold: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle', - { defaultMessage: 'No nodes assigned to the cold tier' } - ), - body: i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody', - { - defaultMessage: - 'Assign at least one node to the cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', - } - ), - }, - }, -}; - -interface Props { - phase: PhaseWithAllocation; - targetNodeRole: DataTierRole; -} - -export const DefaultAllocationNotice: FunctionComponent = ({ phase, targetNodeRole }) => { - return ( - - {i18nTexts.notice[phase].body(targetNodeRole)} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx deleted file mode 100644 index 649eb9f2fcb7f0..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx +++ /dev/null @@ -1,72 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import React, { FunctionComponent } from 'react'; -import { EuiCallOut } from '@elastic/eui'; - -import { PhaseWithAllocation } from '../../../../../../../../../common/types'; - -const i18nTexts = { - warning: { - warm: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle', - { defaultMessage: 'No nodes assigned to the warm tier' } - ), - body: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody', - { - defaultMessage: - 'Assign at least one node to the warm or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', - } - ), - }, - cold: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle', - { defaultMessage: 'No nodes assigned to the cold tier' } - ), - body: i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody', - { - defaultMessage: - 'Assign at least one node to the cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', - } - ), - }, - frozen: { - title: i18n.translate( - 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableTitle', - { defaultMessage: 'No nodes assigned to the frozen tier' } - ), - body: i18n.translate( - 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableBody', - { - defaultMessage: - 'Assign at least one node to the frozen, cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', - } - ), - }, - }, -}; - -interface Props { - phase: PhaseWithAllocation; -} - -export const DefaultAllocationWarning: FunctionComponent = ({ phase }) => { - return ( - - {i18nTexts.warning[phase].body} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_to_data_nodes_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_to_data_nodes_notice.tsx new file mode 100644 index 00000000000000..b29ddb75c18cf7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_to_data_nodes_notice.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { PhaseWithAllocation } from '../../../../../../../../../common/types'; +import { + noCustomAttributesTitle, + nodeAllocationMigrationGuidance, +} from './no_custom_attributes_messages'; + +export const DefaultToDataNodesNotice: FunctionComponent<{ phase: PhaseWithAllocation }> = ({ + phase, +}) => { + return ( + +

+ {i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultToDataNodesDescription', + { defaultMessage: 'Data will be allocated to any available data node.' } + )} +

+ + {nodeAllocationMigrationGuidance} +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_to_data_tiers_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_to_data_tiers_notice.tsx new file mode 100644 index 00000000000000..25414406e67b5d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_to_data_tiers_notice.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { PhaseWithAllocation } from '../../../../../../../../../common/types'; +import { + noCustomAttributesTitle, + nodeAllocationMigrationGuidance, +} from './no_custom_attributes_messages'; + +const i18nTexts = { + body: { + warm: ( + <> +

+ {i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableDescription', + { defaultMessage: 'Data will be allocated to the warm tier.' } + )} +

+ + {nodeAllocationMigrationGuidance} + + ), + cold: ( + <> +

+ {i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableDescription', + { defaultMessage: 'Data will be allocated to the cold tier.' } + )} +

+ + {nodeAllocationMigrationGuidance} + + ), + }, +}; + +export const DefaultToDataTiersNotice: FunctionComponent<{ phase: PhaseWithAllocation }> = ({ + phase, +}) => { + return ( + + {i18nTexts.body[phase]} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts index e9c884a42fa937..ce5685ba983075 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts @@ -11,11 +11,17 @@ export { NodeAttrsDetails } from './node_attrs_details'; export { DataTierAllocation } from './data_tier_allocation'; -export { DefaultAllocationNotice } from './default_allocation_notice'; +export { WillUseFallbackTierNotice } from './will_use_fallback_tier_notice'; -export { DefaultAllocationWarning } from './default_allocation_warning'; +export { WillUseFallbackTierUsingNodeAttributesNotice } from './will_use_fallback_tier_using_node_attributes_notice'; -export { NoNodeAttributesWarning } from './no_node_attributes_warning'; +export { NoTiersAvailableNotice } from './no_tiers_available_notice'; + +export { NoTiersAvailableUsingNodeAttributesNotice } from './no_tiers_available_using_node_attributes_notice'; + +export { DefaultToDataTiersNotice } from './default_to_data_tiers_notice'; + +export { DefaultToDataNodesNotice } from './default_to_data_nodes_notice'; export { CloudDataTierCallout } from './cloud_data_tier_callout'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_custom_attributes_messages.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_custom_attributes_messages.tsx new file mode 100644 index 00000000000000..4d948ac202eb86 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_custom_attributes_messages.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { getNodeAllocationMigrationLink } from '../../../../../../../services/documentation'; + +export const noCustomAttributesTitle = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.noCustomAttributesTitle', + { defaultMessage: 'No custom attributes defined' } +); + +export const nodeAllocationMigrationGuidance = ( + + {i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.defaultToDataNodesDescription.migrationGuidanceMessage', + { + defaultMessage: 'use role-based allocation', + } + )} + + ), + }} + /> +); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx deleted file mode 100644 index e6cadf70499624..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx +++ /dev/null @@ -1,60 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; -import { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { PhaseWithAllocation } from '../../../../../../../../../common/types'; - -const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel', { - defaultMessage: 'No custom node attributes configured', - }), - warm: { - body: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription', - { - defaultMessage: - 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.', - } - ), - }, - cold: { - body: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription', - { - defaultMessage: - 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.', - } - ), - }, - frozen: { - body: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.frozen.nodeAttributesMissingDescription', - { - defaultMessage: - 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.', - } - ), - }, -}; - -export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAllocation }> = ({ - phase, -}) => { - return ( - - {i18nTexts[phase].body} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_tiers_available_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_tiers_available_notice.tsx new file mode 100644 index 00000000000000..d4e8e98e9b2be3 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_tiers_available_notice.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +import { PhaseWithAllocation } from '../../../../../../../../../common/types'; + +const i18nTexts = { + warm: { + title: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.noTiersAvailableTitle', { + defaultMessage: 'No nodes assigned to the warm tier', + }), + body: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.noTiersAvailableBody', { + defaultMessage: + 'To use role-based allocation, assign one or more nodes to the warm or hot tiers. Allocation will fail if there are no available nodes.', + }), + }, + cold: { + title: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.noTiersAvailableTitle', { + defaultMessage: 'No nodes assigned to the cold tier', + }), + body: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.noTiersAvailableBody', { + defaultMessage: + 'To use role-based allocation, assign one or more nodes to the cold, warm, or hot tiers. Allocation will fail if there are no available nodes.', + }), + }, +}; + +interface Props { + phase: PhaseWithAllocation; +} + +export const NoTiersAvailableNotice: FunctionComponent = ({ phase }) => { + return ( + + {i18nTexts[phase].body} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_tiers_available_using_node_attributes_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_tiers_available_using_node_attributes_notice.tsx new file mode 100644 index 00000000000000..02dbd1baff25f0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_tiers_available_using_node_attributes_notice.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +import { + noCustomAttributesTitle, + nodeAllocationMigrationGuidance, +} from './no_custom_attributes_messages'; + +export const NoTiersAvailableUsingNodeAttributesNotice: FunctionComponent = () => { + return ( + +

+ {i18n.translate( + 'xpack.indexLifecycleMgmt.dataTier.noTiersAvailableUsingNodeAttributesDescription', + { + defaultMessage: 'Unable to allocate data: no available data nodes.', + } + )} +

+ +

{nodeAllocationMigrationGuidance}

+
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx index f8f77ed909a91b..e7b1f7420d1320 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx @@ -34,13 +34,19 @@ const learnMoreLink = ( ); const i18nTexts = { - doNotModifyAllocationOption: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption', - { defaultMessage: 'Do not modify allocation configuration' } + allocateToDataNodesOption: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.allocateToDataNodesOption', + { defaultMessage: 'Any data node' } ), }; -export const NodeAllocation: FunctionComponent = ({ phase, nodes, isLoading }) => { +export const NodeAllocation: FunctionComponent = ({ + phase, + nodes, + isLoading, + isCloudEnabled, + isUsingDeprecatedDataRoleConfig, +}) => { const allocationNodeAttributePath = `_meta.${phase}.allocationNodeAttribute`; const [formData] = useFormData({ @@ -60,6 +66,20 @@ export const NodeAllocation: FunctionComponent = ({ phase, nodes, i nodeOptions.sort((a, b) => a.value.localeCompare(b.value)); + let nodeAllocationOptions = []; + + // On Cloud, allocating to data tiers and allocating to data nodes is mutually exclusive. So we + // only let users select this option if they're using data nodes. Otherwise we remove it. + // + // On prem, users should have the freedom to choose this option, even if they're using node roles. + // So we always give them this option. + if (!isCloudEnabled || isUsingDeprecatedDataRoleConfig) { + const allocateToDataNodesOption = { text: i18nTexts.allocateToDataNodesOption, value: '' }; + nodeAllocationOptions.push(allocateToDataNodesOption); + } + + nodeAllocationOptions = nodeAllocationOptions.concat(nodeOptions); + return ( <> @@ -97,9 +117,7 @@ export const NodeAllocation: FunctionComponent = ({ phase, nodes, i ) : undefined, euiFieldProps: { 'data-test-subj': `${phase}-selectedNodeAttrs`, - options: [{ text: i18nTexts.doNotModifyAllocationOption, value: '' }].concat( - nodeOptions - ), + options: nodeAllocationOptions, hasNoInitialSelection: false, isLoading, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_role_to_fallback_tier_map.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_role_to_fallback_tier_map.ts new file mode 100644 index 00000000000000..ad17855f307c71 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_role_to_fallback_tier_map.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +import { DataTierRole } from '../../../../../../../../../common/types'; + +export const nodeRoleToFallbackTierMap: Partial> = { + data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', { + defaultMessage: 'hot', + }), + data_warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel', { + defaultMessage: 'warm', + }), +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts index b78ab720618ef7..aba044e1862ef5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts @@ -14,12 +14,8 @@ export interface SharedProps { phase: PhaseWithAllocation; nodes: ListNodesRouteResponse['nodesByAttributes']; hasNodeAttributes: boolean; - /** - * When on Cloud we want to disable the data tier allocation option when we detect that we are not - * using node roles in our Node config yet. See {@link ListNodesRouteResponse} for information about how this is - * detected. - */ - disableDataTierOption: boolean; + isCloudEnabled: boolean; + isUsingDeprecatedDataRoleConfig: boolean; /** * A flag to indicate whether input fields should be showing a loading spinner */ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/will_use_fallback_tier_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/will_use_fallback_tier_notice.tsx new file mode 100644 index 00000000000000..90ac6e02ef9e21 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/will_use_fallback_tier_notice.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +import { PhaseWithAllocation, DataTierRole } from '../../../../../../../../../common/types'; +import { nodeRoleToFallbackTierMap } from './node_role_to_fallback_tier_map'; + +const i18nTexts = { + warm: { + title: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.willUseFallbackTierTitle', { + defaultMessage: 'No nodes assigned to the warm tier', + }), + body: (nodeRole: DataTierRole) => + i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.willUseFallbackTierDescription', { + defaultMessage: 'If no warm nodes are available, data is stored in the {tier} tier.', + values: { tier: nodeRoleToFallbackTierMap[nodeRole] }, + }), + }, + cold: { + title: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.willUseFallbackTierTitle', { + defaultMessage: 'No nodes assigned to the cold tier', + }), + body: (nodeRole: DataTierRole) => + i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.willUseFallbackTierDescription', { + defaultMessage: 'If no cold nodes are available, data is stored in the {tier} tier.', + values: { tier: nodeRoleToFallbackTierMap[nodeRole] }, + }), + }, +}; + +interface Props { + phase: PhaseWithAllocation; + targetNodeRole: DataTierRole; +} + +export const WillUseFallbackTierNotice: FunctionComponent = ({ phase, targetNodeRole }) => { + return ( + + {i18nTexts[phase].body(targetNodeRole)} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/will_use_fallback_tier_using_node_attributes_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/will_use_fallback_tier_using_node_attributes_notice.tsx new file mode 100644 index 00000000000000..199f1001f42763 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/will_use_fallback_tier_using_node_attributes_notice.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +import { PhaseWithAllocation, DataTierRole } from '../../../../../../../../../common/types'; +import { + noCustomAttributesTitle, + nodeAllocationMigrationGuidance, +} from './no_custom_attributes_messages'; +import { nodeRoleToFallbackTierMap } from './node_role_to_fallback_tier_map'; + +interface Props { + phase: PhaseWithAllocation; + targetNodeRole: DataTierRole; +} + +export const WillUseFallbackTierUsingNodeAttributesNotice: FunctionComponent = ({ + phase, + targetNodeRole, +}) => { + return ( + +

+ {i18n.translate( + 'xpack.indexLifecycleMgmt.dataTier.willUseFallbackTierUsingNodeAttributesDescription', + { + defaultMessage: + 'No {phase} nodes available. Data will be allocated to the {fallbackTier} tier.', + values: { phase, fallbackTier: nodeRoleToFallbackTierMap[targetNodeRole] }, + } + )} +

+ +

{nodeAllocationMigrationGuidance}

+
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index 8c90a738d2c090..c45e1728689389 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -11,20 +11,19 @@ import { i18n } from '@kbn/i18n'; import { EuiDescribedFormGroup, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui'; import { useKibana, useFormData } from '../../../../../../../shared_imports'; - -import { PhaseWithAllocation } from '../../../../../../../../common/types'; - +import { PhaseWithAllocation, DataTierRole } from '../../../../../../../../common/types'; import { getAvailableNodeRoleForPhase, isNodeRoleFirstPreference } from '../../../../../../lib'; - import { useLoadNodes } from '../../../../../../services/api'; - import { DataTierAllocationType } from '../../../../types'; import { DataTierAllocation, - DefaultAllocationNotice, - DefaultAllocationWarning, - NoNodeAttributesWarning, + WillUseFallbackTierNotice, + WillUseFallbackTierUsingNodeAttributesNotice, + NoTiersAvailableNotice, + NoTiersAvailableUsingNodeAttributesNotice, + DefaultToDataNodesNotice, + DefaultToDataTiersNotice, CloudDataTierCallout, LoadingError, } from './components'; @@ -58,31 +57,37 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr const { nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig } = data!; - const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); + const hasNodeAttributes = Boolean(Object.keys(nodesByAttributes ?? {}).length); const isCloudEnabled = cloud?.isCloudEnabled ?? false; const cloudDeploymentUrl = cloud?.deploymentUrl; + const allocationNodeRoleForPhase = getAvailableNodeRoleForPhase(phase, nodesByRoles); + const noTiersAvailable = allocationNodeRoleForPhase === undefined; + const willUseFallbackTier = + allocationNodeRoleForPhase !== undefined && + !isNodeRoleFirstPreference(phase, allocationNodeRoleForPhase); + const renderNotice = () => { switch (allocationType) { case 'node_roles': /** - * On cloud most users should be using autoscaling which will provision tiers as they are needed. We do not surface any + * On Cloud most users should be using autoscaling which will provision tiers as they are needed. We do not surface any * of the notices below. */ if (isCloudEnabled) { return null; } + /** * Node role allocation moves data in a phase to a corresponding tier of the same name. To prevent policy execution from getting * stuck ILM allocation will fall back to a previous tier if possible. We show the WARNING below to inform a user when even * this fallback will not succeed. */ - const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); - if (allocationNodeRole === 'none') { + if (noTiersAvailable) { return ( <> - + ); } @@ -91,26 +96,79 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr * If we are able to fallback to a data tier that does not map to this phase, we show a notice informing the user that their * data will not be assigned to a corresponding tier. */ - if (!isNodeRoleFirstPreference(phase, allocationNodeRole)) { + if (willUseFallbackTier) { return ( <> - + ); } break; + case 'node_attrs': - if (!hasNodeAttrs) { + /** + * If there are no node attributes, advise the user on the default allocation behavior. + */ + if (!hasNodeAttributes) { + /** + * If data nodes are available, default allocation behavior will be to those nodes. + */ + if (isUsingDeprecatedDataRoleConfig) { + return ( + <> + + + + ); + } + + /** + * Node role allocation moves data in a phase to a corresponding tier of the same name. To prevent policy execution from getting + * stuck ILM allocation will fall back to a previous tier if possible. We show the WARNING below to inform a user when even + * this fallback will not succeed, for example if the user only has 'data' node roles, and no `data_` node roles. + */ + if (noTiersAvailable) { + return ( + <> + + + + ); + } + + /** + * If we are able to fallback to a data tier that does not map to this phase, we show a notice informing the user that their + * data will not be assigned to a corresponding tier. + */ + if (willUseFallbackTier) { + return ( + <> + + + + ); + } + + /** + * If using node roles, default allocation behavior will be to the preferred nodes, depending on the phase. + */ return ( <> - + ); } + /** - * Special cloud case: when deprecated data role configuration is in use, it means that this deployment is not using + * Special Cloud case: when deprecated data role configuration is in use, it means that this deployment is not using * the new node role based allocation. We drive users to the cloud console to migrate to node role based allocation * in that case. */ @@ -137,7 +195,7 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr {isLoading ? ( <> - + ) : ( error && ( @@ -154,10 +212,11 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr >
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts index bb14f1d03f31cc..27e571248d66d3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/documentation.ts @@ -22,3 +22,5 @@ export function init(esDocBasePath: string): void { } export const createDocLink = (docPath: string): string => `${_esDocBasePath}${docPath}`; +export const getNodeAllocationMigrationLink = () => + `${_esDocBasePath}migrate-index-allocation-filters.html`; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b1d0478d5e0e17..21e2a38ab7b85d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9909,8 +9909,6 @@ "xpack.indexLifecycleMgmt.appTitle": "インデックスライフサイクルポリシー", "xpack.indexLifecycleMgmt.breadcrumb.editPolicyLabel": "ポリシーの編集", "xpack.indexLifecycleMgmt.breadcrumb.homeLabel": "インデックスライフサイクル管理", - "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody": "ロールに基づく割り当てを使用するには、1 つ以上のノードを、コールド、ウォーム、またはホットティアに割り当てます。使用可能なノードがない場合、ポリシーは割り当てを完了できません。", - "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "コールドティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。安価なハードウェアのコールドフェーズにデータを格納します。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結", "xpack.indexLifecycleMgmt.common.dataTier.title": "データ割り当て", @@ -9923,7 +9921,6 @@ "xpack.indexLifecycleMgmt.editPolicy.cancelButton": "キャンセル", "xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body": "Elastic Cloud デプロイを編集し、色ティアを設定します。", "xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title": "コールドティアを作成", - "xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription": "属性に基づく割り当てを使用するには、elasticsearch.yml でカスタムノード属性を定義します。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "コールドフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription": "データをコールドティアに移動します。これは、検索パフォーマンスよりもコスト削減を優先するように最適化されています。通常、コールドフェーズではデータが読み取り専用です。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle": "コールドフェーズ", @@ -9941,7 +9938,6 @@ "xpack.indexLifecycleMgmt.editPolicy.createSnapshotRepositoryLink": "新しいスナップショットリポジドリを作成", "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.allocationFieldLabel": "データティアオプション", "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.nodeAllocationFieldLabel": "ノード属性を選択", - "xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel": "コールド", "xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel": "ホット", "xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel": "ウォーム", "xpack.indexLifecycleMgmt.editPolicy.daysOptionLabel": "日", @@ -10006,9 +10002,7 @@ "xpack.indexLifecycleMgmt.editPolicy.minutesOptionLabel": "分", "xpack.indexLifecycleMgmt.editPolicy.nanoSecondsOptionLabel": "ナノ秒", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "ノード属性を使用して、シャード割り当てを制御します。{learnMoreLink}。", - "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption": "割り当て構成を修正しない", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle": "ノードデータを読み込めません", - "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "カスタムノード属性が構成されていません", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "再試行", "xpack.indexLifecycleMgmt.editPolicy.nodeDetailsLoadingFailedTitle": "ノード属性詳細を読み込めません", "xpack.indexLifecycleMgmt.editPolicy.nodeDetailsReloadButton": "再試行", @@ -10054,7 +10048,6 @@ "xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage": "ポリシー名の頭にアンダーラインを使用することはできず、カンマやスペースを含めることもできません。", "xpack.indexLifecycleMgmt.editPolicy.viewNodeDetailsButton": "選択した属性のノードを表示", "xpack.indexLifecycleMgmt.editPolicy.waitForSnapshot.snapshotPolicyFieldLabel": "ポリシー名 (任意) ", - "xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription": "属性に基づく割り当てを使用するには、elasticsearch.yml でカスタムノード属性を定義します。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.activateWarmPhaseSwitchLabel": "ウォームフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.indexPriorityExplanationText": "ノードの再起動後にインデックスを復元する優先順位を設定します。優先順位の高いインデックスは優先順位の低いインデックスよりも先に復元されます。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescription": "データをウォームティアに移動します。これは、インデックスパフォーマンスよりも検索パフォーマンスを優先するように最適化されています。ウォームフェーズでは、データの追加または更新頻度は高くありません。", @@ -10198,12 +10191,6 @@ "xpack.indexLifecycleMgmt.timeline.hotPhaseSectionTitle": "ホットフェーズ", "xpack.indexLifecycleMgmt.timeline.title": "ポリシー概要", "xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle": "ウォームフェーズ", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody": "ロールに基づく割り当てを使用するには、1つ以上のノードを、ウォームまたはホットティアに割り当てます。使用可能なノードがない場合、ポリシーは割り当てを完了できません。", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle": "ウォームティアに割り当てられているノードがありません", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold": "このポリシーはコールドフェーズのデータを{tier}ティアノードに移動します。", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold.title": "コールドティアに割り当てられているノードがありません", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "このポリシーはウォームフェーズのデータを{tier}ティアノードに移動します。", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "ウォームティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。", "xpack.infra.alerting.alertDropdownTitle": "アラート", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし (グループなし) ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index afad5591ace757..49fa7a4aaa0a03 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10039,8 +10039,6 @@ "xpack.indexLifecycleMgmt.appTitle": "索引生命周期策略", "xpack.indexLifecycleMgmt.breadcrumb.editPolicyLabel": "编辑策略", "xpack.indexLifecycleMgmt.breadcrumb.homeLabel": "索引生命周期管理", - "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody": "至少将一个节点分配到冷层、温层或热层,以使用基于角色的分配。如果没有可用节点,则策略无法完成分配。", - "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "没有分配到冷层的节点", "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。将处于冷阶段的数据存储在成本较低的硬件上。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引", "xpack.indexLifecycleMgmt.common.dataTier.title": "数据分配", @@ -10053,7 +10051,6 @@ "xpack.indexLifecycleMgmt.editPolicy.cancelButton": "取消", "xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body": "编辑您的 Elastic Cloud 部署以设置冷层。", "xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title": "创建冷层", - "xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription": "在 elasticsearch.yml 中定义定制节点属性,以使用基于属性的分配。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "激活冷阶段", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription": "将数据移到经过优化后节省了成本但牺牲了搜索性能的冷层。数据在冷阶段通常为只读。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle": "冷阶段", @@ -10071,7 +10068,6 @@ "xpack.indexLifecycleMgmt.editPolicy.createSnapshotRepositoryLink": "创建新的快照库", "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.allocationFieldLabel": "数据层选项", "xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.nodeAllocationFieldLabel": "选择节点属性", - "xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel": "冷", "xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel": "热", "xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel": "温", "xpack.indexLifecycleMgmt.editPolicy.daysOptionLabel": "天", @@ -10136,9 +10132,7 @@ "xpack.indexLifecycleMgmt.editPolicy.minutesOptionLabel": "分钟", "xpack.indexLifecycleMgmt.editPolicy.nanoSecondsOptionLabel": "纳秒", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description": "使用节点属性控制分片分配。{learnMoreLink}。", - "xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption": "切勿修改分配配置", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle": "无法加载节点数据", - "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "未配置定制节点属性", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "重试", "xpack.indexLifecycleMgmt.editPolicy.nodeDetailsLoadingFailedTitle": "无法加载节点属性详情", "xpack.indexLifecycleMgmt.editPolicy.nodeDetailsReloadButton": "重试", @@ -10184,7 +10178,6 @@ "xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage": "策略名称不能以下划线开头,且不能包含逗号或空格。", "xpack.indexLifecycleMgmt.editPolicy.viewNodeDetailsButton": "查看具有选定属性的节点", "xpack.indexLifecycleMgmt.editPolicy.waitForSnapshot.snapshotPolicyFieldLabel": "策略名称 (可选) ", - "xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription": "在 elasticsearch.yml 中定义定制节点属性,以使用基于属性的分配。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.activateWarmPhaseSwitchLabel": "激活温阶段", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.indexPriorityExplanationText": "设置在节点重新启动后恢复索引的优先级。较高优先级的索引会在较低优先级的索引之前恢复。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescription": "将数据移到优化了搜索性能但牺牲了索引性能的温层。在温层不经常添加或更新数据。", @@ -10332,12 +10325,6 @@ "xpack.indexLifecycleMgmt.timeline.hotPhaseSectionTitle": "热阶段", "xpack.indexLifecycleMgmt.timeline.title": "策略摘要", "xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle": "温阶段", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody": "至少将一个节点分配到温层或冷层,以使用基于角色的分配。如果没有可用节点,则策略无法完成分配。", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle": "没有分配到温层的节点", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold": "此策略会改为将冷阶段的数据移到{tier}层节点。", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.cold.title": "没有分配到冷层的节点", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "此策略会改为将温阶段的数据移到{tier}层节点。", - "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "没有分配到温层的节点", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。", "xpack.infra.alerting.alertDropdownTitle": "告警", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容 (未分组) ",