Reintroduce 96111: Provide guidance of "Custom" allocation behavior in ILM (#99007)

This commit is contained in:
CJ Cenizal 2021-05-03 12:29:57 -07:00 committed by GitHub
parent ad9f1c3155
commit 9acdfaaf66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1642 additions and 1024 deletions

View file

@ -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<TestSubjects> {
export interface AppTestBed extends TestBed {
actions: {
clickPolicyNameLink: () => void;
clickCreatePolicyButton: () => void;

View file

@ -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) => (
<input
data-test-subj={props['data-test-subj'] || 'mockComboBox'}
data-currentvalue={props.selectedOptions}
onChange={async (syntheticEvent: any) => {
props.onChange([syntheticEvent['0']]);
}}
/>
),
EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now.
};
});
const getTestBedConfig = (testBedConfigArgs?: Partial<TestBedConfig>): TestBedConfig => {
return {
memoryRouter: {
initialEntries: [`/policies/edit/${POLICY_NAME}`],
componentRoutePath: `/policies/edit/:policyName`,
},
defaultProps: {
getUrlForApp: () => {},
},
...testBedConfigArgs,
};
};
const breadcrumbService = createBreadcrumbsMock();
const MyComponent = ({ appServicesContext, ...rest }: any) => {
return (
<KibanaContextProvider
services={{
breadcrumbService,
license: licensingMock.createLicense({ license: { type: 'enterprise' } }),
...appServicesContext,
}}
>
<EditPolicy {...rest} />
</KibanaContextProvider>
);
};
const initTestBed = (arg?: {
appServicesContext?: Partial<AppServicesContext>;
testBedConfig?: Partial<TestBedConfig>;
}) => {
const { testBedConfig: testBedConfigArgs, ...rest } = arg || {};
return registerTestBed<TestSubjects>(MyComponent, getTestBedConfig(testBedConfigArgs))(rest);
};
import {
Phase,
createEnablePhaseAction,
createNodeAllocationActions,
createFormToggleAction,
createFormSetValueAction,
setReplicas,
savePolicy,
} from '../helpers';
import { initTestBed } from './init_test_bed';
type SetupReturn = ReturnType<typeof setup>;
export type EditPolicyTestBed = SetupReturn extends Promise<infer U> ? 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<V extends string = string>(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(testBed, 'useDefaultRolloverSwitch');
const toggleDefaultRollover = createFormToggleAction('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'),

View file

@ -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('<EditPolicy /> 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');
});
});
});
});

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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<typeof setupCloudNodeAllocation>;
export type CloudNodeAllocationTestBed = SetupReturn extends Promise<infer U> ? U : SetupReturn;
export const setupCloudNodeAllocation = async (arg?: {
appServicesContext?: Partial<AppServicesContext>;
testBedConfig?: Partial<TestBedConfig>;
}) => {
const testBed = await initTestBed(arg);
return {
...testBed,
actions: {
warm: {
enable: createEnablePhaseAction(testBed, 'warm'),
...createNodeAllocationActions(testBed, 'warm'),
},
cold: {
enable: createEnablePhaseAction(testBed, 'cold'),
...createNodeAllocationActions(testBed, 'cold'),
},
},
};
};

View file

@ -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('<EditPolicy /> 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();
});
});
});
});

View file

@ -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<typeof setupColdPhaseNodeAllocation>;
export type NodeAllocationTestBed = SetupReturn extends Promise<infer U> ? U : SetupReturn;
export const setupColdPhaseNodeAllocation = async () => {
const testBed = await initTestBed();
return {
...testBed,
actions: {
enable: createEnablePhaseAction(testBed, 'cold'),
...createNodeAllocationActions(testBed, 'cold'),
},
};
};

View file

@ -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('<EditPolicy /> 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.`
);
});
});
});
});

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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<typeof setupGeneralNodeAllocation>;
export type GeneralNodeAllocationTestBed = SetupReturn extends Promise<infer U> ? 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),
},
};
};

View file

@ -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('<EditPolicy /> 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');
});
});
});
});

View file

@ -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<typeof setupWarmPhaseNodeAllocation>;
export type NodeAllocationTestBed = SetupReturn extends Promise<infer U> ? U : SetupReturn;
export const setupWarmPhaseNodeAllocation = async () => {
const testBed = await initTestBed();
return {
...testBed,
actions: {
enable: createEnablePhaseAction(testBed, 'warm'),
...createNodeAllocationActions(testBed, 'warm'),
},
};
};

View file

@ -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('<EditPolicy /> 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.`
);
});
});
});

View file

@ -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>): TestBedConfig => {
return {
memoryRouter: {
initialEntries: [`/policies/edit/${POLICY_NAME}`],
componentRoutePath: `/policies/edit/:policyName`,
},
defaultProps: {
getUrlForApp: () => {},
},
...testBedConfigArgs,
};
};
const breadcrumbService = createBreadcrumbsMock();
const EditPolicyContainer = ({ appServicesContext, ...rest }: any) => {
return (
<KibanaContextProvider
services={{
breadcrumbService,
license: licensingMock.createLicense({ license: { type: 'enterprise' } }),
...appServicesContext,
}}
>
<EditPolicy {...rest} />
</KibanaContextProvider>
);
};
export const initTestBed = (arg?: {
appServicesContext?: Partial<AppServicesContext>;
testBedConfig?: Partial<TestBedConfig>;
}) => {
const { testBedConfig: testBedConfigArgs, ...rest } = arg || {};
return registerTestBed(EditPolicyContainer, getTestBedConfig(testBedConfigArgs))(rest);
};

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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}`);
};

View file

@ -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<V extends string = string>(
testBed: TestBed,
dataTestSubject: string
) {
const { form, component } = testBed;
return async (value: V) => {
await act(async () => {
form.setInputValue(dataTestSubject, value);
});
component.update();
};
}

View file

@ -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();
};

View file

@ -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();
},
};
};

View file

@ -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) => (
<input
data-test-subj={props['data-test-subj'] || 'mockComboBox'}
data-currentvalue={props.selectedOptions}
onChange={async (syntheticEvent: any) => {
props.onChange([syntheticEvent['0']]);
}}
/>
),
EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now.
};
});

View file

@ -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';

View file

@ -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();
};

View file

@ -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);
};

View file

@ -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;

View file

@ -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;

View file

@ -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));
};

View file

@ -161,7 +161,20 @@ const getSelectOptions = (phase: PhaseWithAllocation, disableDataTierOption: boo
].filter(Boolean) as SelectOptions[];
export const DataTierAllocation: FunctionComponent<SharedProps> = (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<SharedProps> = (props) => {
if (disableDataTierOption && field.value === 'node_roles') {
field.setValue('node_attrs');
}
return (
<SuperSelectField
field={field}

View file

@ -1,112 +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, DataTierRole } from '../../../../../../../../../common/types';
const i18nTextsNodeRoleToDataTier: Record<DataTierRole, string> = {
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<Props> = ({ phase, targetNodeRole }) => {
return (
<EuiCallOut data-test-subj="defaultAllocationNotice" title={i18nTexts.notice[phase].title}>
{i18nTexts.notice[phase].body(targetNodeRole)}
</EuiCallOut>
);
};

View file

@ -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<Props> = ({ phase }) => {
return (
<EuiCallOut
data-test-subj="defaultAllocationWarning"
title={i18nTexts.warning[phase].title}
color="warning"
>
{i18nTexts.warning[phase].body}
</EuiCallOut>
);
};

View file

@ -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 (
<EuiCallOut
data-test-subj="defaultToDataNodesNotice"
style={{ maxWidth: 400 }}
title={noCustomAttributesTitle}
color="primary"
>
<p>
{i18n.translate(
'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultToDataNodesDescription',
{ defaultMessage: 'Data will be allocated to any available data node.' }
)}
</p>
{nodeAllocationMigrationGuidance}
</EuiCallOut>
);
};

View file

@ -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: (
<>
<p>
{i18n.translate(
'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableDescription',
{ defaultMessage: 'Data will be allocated to the warm tier.' }
)}
</p>
{nodeAllocationMigrationGuidance}
</>
),
cold: (
<>
<p>
{i18n.translate(
'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableDescription',
{ defaultMessage: 'Data will be allocated to the cold tier.' }
)}
</p>
{nodeAllocationMigrationGuidance}
</>
),
},
};
export const DefaultToDataTiersNotice: FunctionComponent<{ phase: PhaseWithAllocation }> = ({
phase,
}) => {
return (
<EuiCallOut
data-test-subj="defaultToDataTiersNotice"
style={{ maxWidth: 400 }}
title={noCustomAttributesTitle}
color="primary"
>
{i18nTexts.body[phase]}
</EuiCallOut>
);
};

View file

@ -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';

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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 = (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.defaultToDataNodesDescription"
defaultMessage="To allocate data to particular data nodes, {roleBasedGuidance} or configure custom node attributes in elasticsearch.yml."
values={{
roleBasedGuidance: (
<EuiLink href={getNodeAllocationMigrationLink()} target="_blank" external={true}>
{i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.defaultToDataNodesDescription.migrationGuidanceMessage',
{
defaultMessage: 'use role-based allocation',
}
)}
</EuiLink>
),
}}
/>
);

View file

@ -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 (
<EuiCallOut
data-test-subj="noNodeAttributesWarning"
style={{ maxWidth: 400 }}
title={i18nTexts.title}
color="warning"
>
{i18nTexts[phase].body}
</EuiCallOut>
);
};

View file

@ -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<Props> = ({ phase }) => {
return (
<EuiCallOut
data-test-subj="noTiersAvailableNotice"
title={i18nTexts[phase].title}
color="warning"
>
{i18nTexts[phase].body}
</EuiCallOut>
);
};

View file

@ -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 (
<EuiCallOut
data-test-subj="noTiersAvailableUsingNodeAttributesNotice"
title={noCustomAttributesTitle}
color="warning"
>
<p>
{i18n.translate(
'xpack.indexLifecycleMgmt.dataTier.noTiersAvailableUsingNodeAttributesDescription',
{
defaultMessage: 'Unable to allocate data: no available data nodes.',
}
)}
</p>
<p>{nodeAllocationMigrationGuidance}</p>
</EuiCallOut>
);
};

View file

@ -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<SharedProps> = ({ phase, nodes, isLoading }) => {
export const NodeAllocation: FunctionComponent<SharedProps> = ({
phase,
nodes,
isLoading,
isCloudEnabled,
isUsingDeprecatedDataRoleConfig,
}) => {
const allocationNodeAttributePath = `_meta.${phase}.allocationNodeAttribute`;
const [formData] = useFormData({
@ -60,6 +66,20 @@ export const NodeAllocation: FunctionComponent<SharedProps> = ({ 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 (
<>
<EuiText size="s">
@ -97,9 +117,7 @@ export const NodeAllocation: FunctionComponent<SharedProps> = ({ phase, nodes, i
) : undefined,
euiFieldProps: {
'data-test-subj': `${phase}-selectedNodeAttrs`,
options: [{ text: i18nTexts.doNotModifyAllocationOption, value: '' }].concat(
nodeOptions
),
options: nodeAllocationOptions,
hasNoInitialSelection: false,
isLoading,
},

View file

@ -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<Record<DataTierRole, string>> = {
data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', {
defaultMessage: 'hot',
}),
data_warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierWarmLabel', {
defaultMessage: 'warm',
}),
};

View file

@ -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
*/

View file

@ -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<Props> = ({ phase, targetNodeRole }) => {
return (
<EuiCallOut data-test-subj="willUseFallbackTierNotice" title={i18nTexts[phase].title}>
{i18nTexts[phase].body(targetNodeRole)}
</EuiCallOut>
);
};

View file

@ -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<Props> = ({
phase,
targetNodeRole,
}) => {
return (
<EuiCallOut
data-test-subj="willUseFallbackTierUsingNodeAttributesNotice"
title={noCustomAttributesTitle}
>
<p>
{i18n.translate(
'xpack.indexLifecycleMgmt.dataTier.willUseFallbackTierUsingNodeAttributesDescription',
{
defaultMessage:
'No {phase} nodes available. Data will be allocated to the {fallbackTier} tier.',
values: { phase, fallbackTier: nodeRoleToFallbackTierMap[targetNodeRole] },
}
)}
</p>
<p>{nodeAllocationMigrationGuidance}</p>
</EuiCallOut>
);
};

View file

@ -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<Props> = ({ 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 (
<>
<EuiSpacer size="s" />
<DefaultAllocationWarning phase={phase} />
<NoTiersAvailableNotice phase={phase} />
</>
);
}
@ -91,26 +96,79 @@ export const DataTierAllocationField: FunctionComponent<Props> = ({ 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 (
<>
<EuiSpacer size="s" />
<DefaultAllocationNotice phase={phase} targetNodeRole={allocationNodeRole} />
<WillUseFallbackTierNotice
phase={phase}
targetNodeRole={allocationNodeRoleForPhase as DataTierRole}
/>
</>
);
}
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 (
<>
<EuiSpacer size="s" />
<DefaultToDataNodesNotice phase={phase} />
</>
);
}
/**
* 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_<tier>` node roles.
*/
if (noTiersAvailable) {
return (
<>
<EuiSpacer size="s" />
<NoTiersAvailableUsingNodeAttributesNotice />
</>
);
}
/**
* 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 (
<>
<EuiSpacer size="s" />
<WillUseFallbackTierUsingNodeAttributesNotice
phase={phase}
targetNodeRole={allocationNodeRoleForPhase as DataTierRole}
/>
</>
);
}
/**
* If using node roles, default allocation behavior will be to the preferred nodes, depending on the phase.
*/
return (
<>
<EuiSpacer size="s" />
<NoNodeAttributesWarning phase={phase} />
<DefaultToDataTiersNotice phase={phase} />
</>
);
}
/**
* 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<Props> = ({ phase, descr
{isLoading ? (
<>
<EuiSpacer size="m" />
<EuiLoadingSpinner size="xl" />
<EuiLoadingSpinner data-test-subj="allocationLoadingSpinner" size="xl" />
</>
) : (
error && (
@ -154,10 +212,11 @@ export const DataTierAllocationField: FunctionComponent<Props> = ({ phase, descr
>
<div className="ilmDataTierAllocationField">
<DataTierAllocation
hasNodeAttributes={hasNodeAttrs}
hasNodeAttributes={hasNodeAttributes}
phase={phase}
nodes={nodesByAttributes}
disableDataTierOption={Boolean(isCloudEnabled && isUsingDeprecatedDataRoleConfig)}
isCloudEnabled={isCloudEnabled}
isUsingDeprecatedDataRoleConfig={isUsingDeprecatedDataRoleConfig}
isLoading={isLoading}
/>

View file

@ -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`;

View file

@ -9914,8 +9914,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": "データ割り当て",
@ -9928,7 +9926,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": "コールドフェーズ",
@ -9946,7 +9943,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": "日",
@ -10011,9 +10007,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": "再試行",
@ -10059,7 +10053,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": "データをウォームティアに移動します。これは、インデックスパフォーマンスよりも検索パフォーマンスを優先するように最適化されています。ウォームフェーズでは、データの追加または更新頻度は高くありません。",
@ -10203,12 +10196,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": "なし (グループなし) ",

View file

@ -10045,8 +10045,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": "数据分配",
@ -10059,7 +10057,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": "冷阶段",
@ -10077,7 +10074,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": "天",
@ -10142,9 +10138,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": "重试",
@ -10190,7 +10184,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": "将数据移到优化了搜索性能但牺牲了索引性能的温层。在温层不经常添加或更新数据。",
@ -10338,12 +10331,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": "无内容 (未分组) ",