[ILM] Data tiers for 7.10 (#76126)

* wip

* Revert "wip"

This reverts commit 54b6f7ff3ec8b0f57b150ab2276d617686da9fb5.

* Revert "Revert "wip""

This reverts commit 63868b44ec60d7431c3a0189b16aeece1db2d38e.

* Refactor to using EUI button group component

- also moved node attr and node allocation component to inside
  new folder that contains all allocation components.
- only focussed on updating warm phase for now

* WIP: moved form UX more in line with EUI

- The described form group now has a switch for showing
  controls on the left.
- Refactored DataTierAllocation to CustomDataTierAllocation
- Removed 'node-roles' option
- Updated copy
- Moved JSX around a bit in the edit policy section to make logic
  simpler

* Refactor UI to reflect custom-ness of "Custom" and "None" options

- Still only implemented for warm, cold and frozen are still
  under way

* server side changes for getting node data

* double opt-in

* Refactored data tier allocation type

- Made types more explicit 'default', 'custom' and 'none'
- Fixed issue introduced by use useCallback on state setter -
  need to use the function setter pattern to not have stale data
  being set.

* Some refacoring, but main point is to add warning detection for
node roles.

- Refactored way we get node data to a provider component so that
  phases still have flexibility in how they render. Currently
  this also means that we fetch node stats data for each phase
  we render
- Created a callout for when there is no node role to which data
  can be allocated for the default setting
- Also updated the behaviour to render the entire form even when
  we cannot fetch node data for some reason. It is not ideal to
  not have node data, but we should not block the entire form.

* fix i18n

* fix type issue with deafult policies missing allocation type

* remove "undefined" as option for setting phase data

* Create referentially stable data setter for all phases - prevent infini-update

* fix type issue

* refactor data -> nodesData

* refactor cold phase for data tiers

* refactor frozen phase for data tiers

* fixed existing tests for warm section

* restored existing test coverage for cold phase and added test coverage for frozen

* fix api integration test

* remove unused translations

* slight UX update to turning on custom attribute allocation

* added scss file for data tier advanced section and other style
updates

* added tests for new warning

* fix types

* added correct copy for cold and frozen phases

* fix types and i18n

* implement copy feedback

* added spacer after the enable data tier allocation switch

* refactor to super select

* fix replicas copy

* update phase serialization for cold and frozen

* Refactor so that logic determining warnings lives together

- also factor out the warning of the node allocation component
- revisit copy for the allocation warning

* tier -> phase

* Added some much needed policy serialization test coverage

- also factored out policy allocation action serialization

* fix import paths and added required file header

* fix existing test coverage

* refine copy for data tier allocation recommended option

* fix showing warning for no node attrs

* fix inverted warning logic 🤦🏼‍♂️

* fix typing

* implement CJs copy feedback

* fix i18n

* remove unused or invalid translations

* provide ability to not alter original policy

* do not alter the original policy in the serilalization process

* fix jest tests

* Remove duplicate type and refactor NodeRole to NodeDataRole

Also deleted unused component "AdvancedSection" for now

* added comment to "false" typing

* revised and refactored copy based on feedback

* address copy feedback

* update kibana schema to allow migrate: { enabled: false }

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2020-09-18 12:14:49 +02:00 committed by GitHub
parent b9958babf3
commit 63bb3bf309
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 2028 additions and 558 deletions

View file

@ -14,8 +14,8 @@ import { SinonFakeServer } from 'sinon';
import { ReactWrapper } from 'enzyme';
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import { createMemoryHistory } from 'history';
import { init as initHttpRequests } from './helpers/http_requests';
import {
notificationServiceMock,
fatalErrorsServiceMock,
@ -41,8 +41,7 @@ import {
policyNameAlreadyUsedErrorMessage,
maximumDocumentsRequiredMessage,
} from '../../public/application/services/policies/policy_validation';
import { HttpResponse } from './helpers/http_requests';
import { createMemoryHistory } from 'history';
import { editPolicyHelpers } from './helpers';
// @ts-ignore
initHttp(axios.create({ adapter: axiosXhrAdapter }));
@ -54,11 +53,8 @@ initNotification(
const history = createMemoryHistory();
let server: SinonFakeServer;
let httpRequestsMockHelpers: {
setPoliciesResponse: (response: HttpResponse) => void;
setNodesListResponse: (response: HttpResponse) => void;
setNodesDetailsResponse: (nodeAttributes: string, response: HttpResponse) => void;
};
let httpRequestsMockHelpers: editPolicyHelpers.EditPolicySetup['http']['httpRequestsMockHelpers'];
let http: editPolicyHelpers.EditPolicySetup['http'];
const policy = {
phases: {
hot: {
@ -94,6 +90,17 @@ const activatePhase = async (rendered: ReactWrapper, phase: string) => {
});
rendered.update();
};
const openNodeAttributesSection = (rendered: ReactWrapper, phase: string) => {
const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`);
act(() => {
findTestSubject(getControls(), 'dataTierSelect').simulate('click');
});
rendered.update();
act(() => {
findTestSubject(getControls(), 'customDataAllocationOption').simulate('click');
});
rendered.update();
};
const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[]) => {
const errorMessages = rendered.find('.euiFormErrorText');
expect(errorMessages.length).toBe(expectedMessages.length);
@ -119,12 +126,16 @@ const setPolicyName = (rendered: ReactWrapper, policyName: string) => {
policyNameField.simulate('change', { target: { value: policyName } });
rendered.update();
};
const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string) => {
const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string | number) => {
const afterInput = rendered.find(`input#${phase}-selectedMinimumAge`);
afterInput.simulate('change', { target: { value: after } });
rendered.update();
};
const setPhaseIndexPriority = (rendered: ReactWrapper, phase: string, priority: string) => {
const setPhaseIndexPriority = (
rendered: ReactWrapper,
phase: string,
priority: string | number
) => {
const priorityInput = rendered.find(`input#${phase}-phaseIndexPriority`);
priorityInput.simulate('change', { target: { value: priority } });
rendered.update();
@ -139,7 +150,9 @@ describe('edit policy', () => {
component = (
<EditPolicy history={history} getUrlForApp={jest.fn()} policies={policies} policyName={''} />
);
({ server, httpRequestsMockHelpers } = initHttpRequests());
({ http } = editPolicyHelpers.setup());
({ server, httpRequestsMockHelpers } = http);
httpRequestsMockHelpers.setPoliciesResponse(policies);
});
@ -321,7 +334,7 @@ describe('edit policy', () => {
describe('warm phase', () => {
beforeEach(() => {
server.respondImmediately = true;
httpRequestsMockHelpers.setNodesListResponse({});
http.setupNodeListResponse();
httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [
{ nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } },
]);
@ -431,34 +444,39 @@ describe('edit policy', () => {
expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy();
});
test('should show warning instead of node attributes input when none exist', async () => {
http.setupNodeListResponse({
nodesByAttributes: {},
nodesByRoles: { data: ['node1'] },
});
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy();
openNodeAttributesSection(rendered, 'warm');
expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy();
expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy();
});
test('should show node attributes input when attributes exist', async () => {
httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] });
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
openNodeAttributesSection(rendered, 'warm');
expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm');
expect(nodeAttributesSelect.exists()).toBeTruthy();
expect(nodeAttributesSelect.find('option').length).toBe(2);
});
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] });
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
openNodeAttributesSection(rendered, 'warm');
expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm');
expect(nodeAttributesSelect.exists()).toBeTruthy();
expect(findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton').exists()).toBeFalsy();
@ -473,11 +491,23 @@ describe('edit policy', () => {
rendered.update();
expect(rendered.find('.euiFlyout').exists()).toBeTruthy();
});
test('should show default allocation warning when no node roles are found', async () => {
http.setupNodeListResponse({
nodesByAttributes: {},
nodesByRoles: {},
});
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy();
});
});
describe('cold phase', () => {
beforeEach(() => {
server.respondImmediately = true;
httpRequestsMockHelpers.setNodesListResponse({});
http.setupNodeListResponse();
httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [
{ nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } },
]);
@ -511,34 +541,39 @@ describe('edit policy', () => {
expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy();
});
test('should show warning instead of node attributes input when none exist', async () => {
http.setupNodeListResponse({
nodesByAttributes: {},
nodesByRoles: { data: ['node1'] },
});
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy();
openNodeAttributesSection(rendered, 'cold');
expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy();
expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy();
});
test('should show node attributes input when attributes exist', async () => {
httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] });
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
openNodeAttributesSection(rendered, 'cold');
expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold');
expect(nodeAttributesSelect.exists()).toBeTruthy();
expect(nodeAttributesSelect.find('option').length).toBe(2);
});
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] });
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
openNodeAttributesSection(rendered, 'cold');
expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold');
expect(nodeAttributesSelect.exists()).toBeTruthy();
expect(findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton').exists()).toBeFalsy();
@ -563,6 +598,128 @@ describe('edit policy', () => {
save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
test('should show default allocation warning when no node roles are found', async () => {
http.setupNodeListResponse({
nodesByAttributes: {},
nodesByRoles: {},
});
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy();
});
});
describe('frozen phase', () => {
beforeEach(() => {
server.respondImmediately = true;
http.setupNodeListResponse();
httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [
{ nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } },
]);
});
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'frozen');
setPhaseAfter(rendered, 'frozen', 0);
save(rendered);
expectedErrorMessages(rendered, []);
});
test('should show positive number required error when trying to save cold phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'frozen');
setPhaseAfter(rendered, 'frozen', -1);
save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
test('should show spinner for node attributes input when loading', async () => {
server.respondImmediately = false;
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'frozen');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
expect(getNodeAttributeSelect(rendered, 'frozen').exists()).toBeFalsy();
});
test('should show warning instead of node attributes input when none exist', async () => {
http.setupNodeListResponse({
nodesByAttributes: {},
nodesByRoles: { data: ['node1'] },
});
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'frozen');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
openNodeAttributesSection(rendered, 'frozen');
expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy();
expect(getNodeAttributeSelect(rendered, 'frozen').exists()).toBeFalsy();
});
test('should show node attributes input when attributes exist', async () => {
http.setupNodeListResponse();
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'frozen');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
openNodeAttributesSection(rendered, 'frozen');
expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'frozen');
expect(nodeAttributesSelect.exists()).toBeTruthy();
expect(nodeAttributesSelect.find('option').length).toBe(2);
});
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
http.setupNodeListResponse();
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'frozen');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
openNodeAttributesSection(rendered, 'frozen');
expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'frozen');
expect(nodeAttributesSelect.exists()).toBeTruthy();
expect(findTestSubject(rendered, 'frozen-viewNodeDetailsFlyoutButton').exists()).toBeFalsy();
expect(nodeAttributesSelect.find('option').length).toBe(2);
nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } });
rendered.update();
const flyoutButton = findTestSubject(rendered, 'frozen-viewNodeDetailsFlyoutButton');
expect(flyoutButton.exists()).toBeTruthy();
await act(async () => {
await flyoutButton.simulate('click');
});
rendered.update();
expect(rendered.find('.euiFlyout').exists()).toBeTruthy();
});
test('should show positive number required error when trying to save with -1 for index priority', async () => {
http.setupNodeListResponse();
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'frozen');
setPhaseAfter(rendered, 'frozen', 1);
setPhaseIndexPriority(rendered, 'frozen', -1);
save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
test('should show default allocation warning when no node roles are found', async () => {
http.setupNodeListResponse({
nodesByAttributes: {},
nodesByRoles: {},
});
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'frozen');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy();
});
});
describe('delete phase', () => {
test('should allow 0 for phase timing', async () => {

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { init as initHttpRequests } from './http_requests';
export type EditPolicySetup = ReturnType<typeof setup>;
export const setup = () => {
const { httpRequestsMockHelpers, server } = initHttpRequests();
const setupNodeListResponse = (
response: Record<string, any> = {
nodesByAttributes: { 'attribute:true': ['node1'] },
nodesByRoles: { data: ['node1'] },
}
) => {
httpRequestsMockHelpers.setNodesListResponse(response);
};
return {
http: {
setupNodeListResponse,
httpRequestsMockHelpers,
server,
},
};
};

View file

@ -40,6 +40,8 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
};
};
export type HttpRequestMockHelpers = ReturnType<typeof registerHttpRequestMockHelpers>;
export const init = () => {
const server = sinon.fakeServer.create();

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as editPolicyHelpers from './edit_policy';
export { HttpRequestMockHelpers, init } from './http_requests';
export { editPolicyHelpers };

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export type NodeDataRole = 'data' | 'data_hot' | 'data_warm' | 'data_cold' | 'data_frozen';
export interface ListNodesRouteResponse {
nodesByAttributes: { [attributePair: string]: string[] };
nodesByRoles: { [role in NodeDataRole]?: string[] };
}

View file

@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from './api';
export * from './policies';

View file

@ -6,6 +6,8 @@
import { Index as IndexInterface } from '../../../index_management/common/types';
export type PhaseWithAllocation = 'warm' | 'cold' | 'frozen';
export interface SerializedPolicy {
name: string;
phases: Phases;
@ -62,6 +64,7 @@ export interface SerializedWarmPhase extends SerializedPhase {
set_priority?: {
priority: number | null;
};
migrate?: { enabled: boolean };
};
}
@ -72,6 +75,7 @@ export interface SerializedColdPhase extends SerializedPhase {
set_priority?: {
priority: number | null;
};
migrate?: { enabled: boolean };
};
}
@ -82,6 +86,7 @@ export interface SerializedFrozenPhase extends SerializedPhase {
set_priority?: {
priority: number | null;
};
migrate?: { enabled: boolean };
};
}
@ -103,6 +108,13 @@ export interface AllocateAction {
require?: {
[attribute: string]: string;
};
migrate?: {
/**
* If enabled is ever set it will only be set to `false` because the default value
* for this is `true`. Rather leave unspecified for true.
*/
enabled: false;
};
}
export interface Policy {
@ -125,9 +137,23 @@ export interface PhaseWithMinAge {
selectedMinimumAgeUnits: string;
}
/**
* Different types of allocation markers we use in deserialized policies.
*
* default - use data tier based data allocation based on node roles -- this is ES best practice mode.
* custom - use node_attrs to allocate data to specific nodes
* none - do not move data anywhere when entering a phase
*/
export type DataTierAllocationType = 'default' | 'custom' | 'none';
export interface PhaseWithAllocationAction {
selectedNodeAttrs: string;
selectedReplicaCount: string;
/**
* A string value indicating allocation type. If unspecified we assume the user
* wants to use default allocation.
*/
dataTierAllocationType: DataTierAllocationType;
}
export interface PhaseWithIndexPriority {

View file

@ -38,6 +38,7 @@ export const defaultNewWarmPhase: WarmPhase = {
selectedReplicaCount: '',
warmPhaseOnRollover: true,
phaseIndexPriority: '50',
dataTierAllocationType: 'default',
};
export const defaultNewColdPhase: ColdPhase = {
@ -48,6 +49,7 @@ export const defaultNewColdPhase: ColdPhase = {
selectedReplicaCount: '',
freezeEnabled: false,
phaseIndexPriority: '0',
dataTierAllocationType: 'default',
};
export const defaultNewFrozenPhase: FrozenPhase = {
@ -58,6 +60,7 @@ export const defaultNewFrozenPhase: FrozenPhase = {
selectedReplicaCount: '',
freezeEnabled: false,
phaseIndexPriority: '0',
dataTierAllocationType: 'default',
};
export const defaultNewDeletePhase: DeletePhase = {

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;
* you may not use this file except in compliance with the Elastic License.
*/
import {
NodeDataRole,
ListNodesRouteResponse,
PhaseWithAllocation,
} from '../../../../common/types';
/**
* Given a phase and current node roles, determine whether the phase
* can use default data tier allocation.
*
* This can only be checked for phases that have an allocate action.
*/
export const isPhaseDefaultDataAllocationCompatible = (
phase: PhaseWithAllocation,
nodesByRoles: ListNodesRouteResponse['nodesByRoles']
): boolean => {
// The 'data' role covers all node roles, so if we have at least one node with the data role
// we can use default allocation.
if (nodesByRoles.data?.length) {
return true;
}
// Otherwise we need to check whether a node role for the specific phase exists
if (nodesByRoles[`data_${phase}` as NodeDataRole]?.length) {
return true;
}
// Otherwise default allocation has nowhere to allocate new shards to in this phase.
return false;
};

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;
* you may not use this file except in compliance with the Elastic License.
*/
import { DataTierAllocationType, AllocateAction } from '../../../../common/types';
/**
* Determine what deserialized state the policy config represents.
*
* See {@DataTierAllocationType} for more information.
*/
export const determineDataTierAllocationType = (
allocateAction?: AllocateAction
): DataTierAllocationType => {
if (!allocateAction) {
return 'default';
}
if (allocateAction.migrate?.enabled === false) {
return 'none';
}
if (
(allocateAction.require && Object.keys(allocateAction.require).length) ||
(allocateAction.include && Object.keys(allocateAction.include).length) ||
(allocateAction.exclude && Object.keys(allocateAction.exclude).length)
) {
return 'custom';
}
return 'default';
};

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './determine_allocation_type';
export * from './check_phase_compatibility';

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './data_tiers';

View file

@ -0,0 +1,9 @@
.indexLifecycleManagement__phase__dataTierAllocation {
&__controlSection {
background-color: $euiColorLightestShade;
padding-top: $euiSizeM;
padding-left: $euiSizeM;
padding-right: $euiSizeM;
padding-bottom: $euiSizeM;
}
}

View file

@ -0,0 +1,194 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiFormRow, EuiSpacer, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui';
import { DataTierAllocationType } from '../../../../../../common/types';
import { NodeAllocation } from './node_allocation';
import { SharedProps } from './types';
import './data_tier_allocation.scss';
type SelectOptions = EuiSuperSelectOption<DataTierAllocationType>;
const i18nTexts = {
allocationFieldLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.allocationFieldLabel',
{ defaultMessage: 'Data tier options' }
),
allocationOptions: {
warm: {
default: {
input: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input',
{ defaultMessage: 'Use warm nodes (recommended)' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText',
{ defaultMessage: 'Move data to nodes in the warm tier.' }
),
},
none: {
inputDisplay: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.input',
{ defaultMessage: 'Off' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText',
{ defaultMessage: 'Do not move data in the warm phase.' }
),
},
custom: {
inputDisplay: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input',
{ defaultMessage: 'Custom' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText',
{ defaultMessage: 'Move data based on node attributes.' }
),
},
},
cold: {
default: {
input: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input',
{ defaultMessage: 'Use cold nodes (recommended)' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText',
{ defaultMessage: 'Move data to nodes in the cold tier.' }
),
},
none: {
inputDisplay: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input',
{ defaultMessage: 'Off' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText',
{ defaultMessage: 'Do not move data in the cold phase.' }
),
},
custom: {
inputDisplay: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input',
{ defaultMessage: 'Custom' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText',
{ defaultMessage: 'Move data based on node attributes.' }
),
},
},
frozen: {
default: {
input: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.input',
{ defaultMessage: 'Use frozen nodes (recommended)' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.helpText',
{ defaultMessage: 'Move data to nodes in the frozen tier.' }
),
},
none: {
inputDisplay: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.input',
{ defaultMessage: 'Off' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.helpText',
{ defaultMessage: 'Do not move data in the frozen phase.' }
),
},
custom: {
inputDisplay: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.customOption.input',
{ defaultMessage: 'Custom' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.customOption.helpText',
{ defaultMessage: 'Move data based on node attributes.' }
),
},
},
},
};
export const DataTierAllocation: FunctionComponent<SharedProps> = (props) => {
const { phaseData, setPhaseData, phase, hasNodeAttributes } = props;
return (
<div data-test-subj={`${phase}-dataTierAllocationControls`}>
<EuiFormRow label={i18nTexts.allocationFieldLabel}>
<EuiSuperSelect
data-test-subj="dataTierSelect"
hasDividers
valueOfSelected={phaseData.dataTierAllocationType}
onChange={(value) => setPhaseData('dataTierAllocationType', value)}
options={
[
{
value: 'default',
inputDisplay: i18nTexts.allocationOptions[phase].default.input,
dropdownDisplay: (
<>
<strong>{i18nTexts.allocationOptions[phase].default.input}</strong>
<EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">
{i18nTexts.allocationOptions[phase].default.helpText}
</p>
</EuiText>
</>
),
},
{
value: 'none',
inputDisplay: i18nTexts.allocationOptions[phase].none.inputDisplay,
dropdownDisplay: (
<>
<strong>{i18nTexts.allocationOptions[phase].none.inputDisplay}</strong>
<EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">
{i18nTexts.allocationOptions[phase].none.helpText}
</p>
</EuiText>
</>
),
},
{
'data-test-subj': 'customDataAllocationOption',
value: 'custom',
inputDisplay: i18nTexts.allocationOptions[phase].custom.inputDisplay,
dropdownDisplay: (
<>
<strong>{i18nTexts.allocationOptions[phase].custom.inputDisplay}</strong>
<EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">
{i18nTexts.allocationOptions[phase].custom.helpText}
</p>
</EuiText>
</>
),
},
] as SelectOptions[]
}
/>
</EuiFormRow>
{phaseData.dataTierAllocationType === 'custom' && hasNodeAttributes && (
<>
<EuiSpacer size="s" />
<div className="indexLifecycleManagement__phase__dataTierAllocation__controlSection">
<NodeAllocation {...props} />
</div>
</>
)}
</div>
);
};

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { PhaseWithAllocation } from '../../../../../../common/types';
const i18nTexts = {
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 tier to use role-based allocation. The policy will fail to complete allocation if there are no warm 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 tier to use role-based allocation. The policy will fail to complete allocation if there are no cold 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 tier to use role-based allocation. The policy will fail to complete allocation if there are no frozen nodes.',
}
),
},
};
interface Props {
phase: PhaseWithAllocation;
}
export const DefaultAllocationWarning: FunctionComponent<Props> = ({ phase }) => {
return (
<>
<EuiSpacer size="s" />
<EuiCallOut
data-test-subj="defaultAllocationWarning"
title={i18nTexts[phase].title}
color="warning"
>
{i18nTexts[phase].body}
</EuiCallOut>
</>
);
};

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { NodesDataProvider } from './node_data_provider';
export { NodeAllocation } from './node_allocation';
export { NodeAttrsDetails } from './node_attrs_details';
export { DataTierAllocation } from './data_tier_allocation';
export { DefaultAllocationWarning } from './default_allocation_warning';
export { NoNodeAttributesWarning } from './no_node_attributes_warning';

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent } from 'react';
import { EuiCallOut, EuiSpacer } 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. Warm nodes will be used instead.',
}
),
},
cold: {
body: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription',
{
defaultMessage:
'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Cold nodes will be used instead.',
}
),
},
frozen: {
body: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.frozen.nodeAttributesMissingDescription',
{
defaultMessage:
'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Frozen nodes will be used instead.',
}
),
},
};
export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAllocation }> = ({
phase,
}) => {
return (
<>
<EuiSpacer size="s" />
<EuiCallOut
data-test-subj="noNodeAttributesWarning"
style={{ maxWidth: 400 }}
title={i18nTexts.title}
color="warning"
>
{i18nTexts[phase].body}
</EuiCallOut>
</>
);
};

View file

@ -0,0 +1,121 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiSelect, EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui';
import { PhaseWithAllocationAction } from '../../../../../../common/types';
import { propertyof } from '../../../../services/policies/policy_validation';
import { ErrableFormRow } from '../form_errors';
import { NodeAttrsDetails } from './node_attrs_details';
import { SharedProps } from './types';
import { LearnMoreLink } from '../learn_more_link';
const learnMoreLink = (
<LearnMoreLink
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink"
defaultMessage="Learn about shard allocation"
/>
}
docPath="modules-cluster.html#cluster-shard-allocation-settings"
/>
);
const i18nTexts = {
doNotModifyAllocationOption: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption',
{ defaultMessage: 'Do not modify allocation configuration' }
),
};
export const NodeAllocation: FunctionComponent<SharedProps> = ({
phase,
setPhaseData,
errors,
phaseData,
isShowingErrors,
nodes,
}) => {
const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState<string | null>(
null
);
const nodeOptions = Object.keys(nodes).map((attrs) => ({
text: `${attrs} (${nodes[attrs].length})`,
value: attrs,
}));
nodeOptions.sort((a, b) => a.value.localeCompare(b.value));
// check that this string is a valid property
const nodeAttrsProperty = propertyof<PhaseWithAllocationAction>('selectedNodeAttrs');
return (
<>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.customOption.description"
defaultMessage="Use node attributes to control shard allocation. {learnMoreLink}."
values={{ learnMoreLink }}
/>
</p>
</EuiText>
<EuiSpacer size="m" />
{/*
TODO: this field component must be revisited to support setting multiple require values and to support
setting `include and exclude values on ILM policies. See https://github.com/elastic/kibana/issues/77344
*/}
<ErrableFormRow
id={`${phase}-${nodeAttrsProperty}`}
label={i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel', {
defaultMessage: 'Select a node attribute',
})}
isShowingErrors={isShowingErrors}
errors={errors?.selectedNodeAttrs}
helpText={
!!phaseData.selectedNodeAttrs ? (
<EuiButtonEmpty
size="xs"
style={{ maxWidth: 400 }}
data-test-subj={`${phase}-viewNodeDetailsFlyoutButton`}
flush="left"
onClick={() => setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)}
>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.viewNodeDetailsButton"
defaultMessage="View nodes with the selected attribute"
/>
</EuiButtonEmpty>
) : null
}
>
<EuiSelect
id={`${phase}-${nodeAttrsProperty}`}
value={phaseData.selectedNodeAttrs || ' '}
options={[{ text: i18nTexts.doNotModifyAllocationOption, value: '' }].concat(nodeOptions)}
onChange={(e) => {
setPhaseData(nodeAttrsProperty, e.target.value);
}}
/>
</ErrableFormRow>
{selectedNodeAttrsForDetails ? (
<NodeAttrsDetails
selectedNodeAttrs={selectedNodeAttrsForDetails}
close={() => setSelectedNodeAttrsForDetails(null)}
/>
) : null}
</>
);
};

View file

@ -20,7 +20,7 @@ import {
EuiButton,
} from '@elastic/eui';
import { useLoadNodeDetails } from '../../../services/api';
import { useLoadNodeDetails } from '../../../../services/api';
interface Props {
close: () => void;

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ListNodesRouteResponse } from '../../../../../../common/types';
import { useLoadNodes } from '../../../../services/api';
interface Props {
children: (data: ListNodesRouteResponse) => JSX.Element;
}
export const NodesDataProvider = ({ children }: Props): JSX.Element => {
const { isLoading, data, error, resendRequest } = useLoadNodes();
if (isLoading) {
return (
<>
<EuiLoadingSpinner size="xl" />
<EuiSpacer size="m" />
</>
);
}
const renderError = () => {
if (error) {
const { statusCode, message } = error;
return (
<>
<EuiCallOut
style={{ maxWidth: 400 }}
title={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle"
defaultMessage="Unable to load node attributes"
/>
}
color="danger"
>
<p>
{message} ({statusCode})
</p>
<EuiButton onClick={resendRequest} iconType="refresh" color="danger">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton"
defaultMessage="Try again"
/>
</EuiButton>
</EuiCallOut>
<EuiSpacer size="xl" />
</>
);
}
return null;
};
return (
<>
{renderError()}
{/* `data` will always be defined because we use an initial value when loading */}
{children(data!)}
</>
);
};

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
ListNodesRouteResponse,
PhaseWithAllocation,
PhaseWithAllocationAction,
} from '../../../../../../common/types';
import { PhaseValidationErrors } from '../../../../services/policies/policy_validation';
export interface SharedProps {
phase: PhaseWithAllocation;
errors?: PhaseValidationErrors<PhaseWithAllocationAction>;
phaseData: PhaseWithAllocationAction;
setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void;
isShowingErrors: boolean;
nodes: ListNodesRouteResponse['nodesByAttributes'];
hasNodeAttributes: boolean;
}

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent } from 'react';
import { EuiDescribedFormGroup, EuiDescribedFormGroupProps } from '@elastic/eui';
import { ToggleableField, Props as ToggleableFieldProps } from './toggleable_field';
type Props = EuiDescribedFormGroupProps & {
switchProps: ToggleableFieldProps;
};
export const DescribedFormField: FunctionComponent<Props> = ({
children,
switchProps,
...restDescribedFormProps
}) => {
return (
<EuiDescribedFormGroup {...restDescribedFormProps}>
<ToggleableField {...switchProps}>{children}</ToggleableField>
</EuiDescribedFormGroup>
);
};

View file

@ -8,11 +8,17 @@ export { ActiveBadge } from './active_badge';
export { ErrableFormRow } from './form_errors';
export { LearnMoreLink } from './learn_more_link';
export { MinAgeInput } from './min_age_input';
export { NodeAllocation } from './node_allocation';
export { NodeAttrsDetails } from './node_attrs_details';
export { OptionalLabel } from './optional_label';
export { PhaseErrorMessage } from './phase_error_message';
export { PolicyJsonFlyout } from './policy_json_flyout';
export { SetPriorityInput } from './set_priority_input';
export { SnapshotPolicies } from './snapshot_policies';
export {
DataTierAllocation,
NodeAllocation,
NodeAttrsDetails,
NodesDataProvider,
DefaultAllocationWarning,
} from './data_tier_allocation';
export { DescribedFormField } from './described_form_field';
export { Forcemerge } from './forcemerge';

View file

@ -1,189 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiSelect,
EuiButtonEmpty,
EuiCallOut,
EuiSpacer,
EuiLoadingSpinner,
EuiButton,
} from '@elastic/eui';
import { LearnMoreLink } from './learn_more_link';
import { ErrableFormRow } from './form_errors';
import { useLoadNodes } from '../../../services/api';
import { NodeAttrsDetails } from './node_attrs_details';
import { PhaseWithAllocationAction, Phases } from '../../../../../common/types';
import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation';
const learnMoreLink = (
<Fragment>
<EuiSpacer size="m" />
<LearnMoreLink
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink"
defaultMessage="Learn about shard allocation"
/>
}
docPath="modules-cluster.html#cluster-shard-allocation-settings"
/>
</Fragment>
);
interface Props<T extends PhaseWithAllocationAction> {
phase: keyof Phases & string;
errors?: PhaseValidationErrors<T>;
phaseData: T;
setPhaseData: (dataKey: keyof T & string, value: string) => void;
isShowingErrors: boolean;
}
export const NodeAllocation = <T extends PhaseWithAllocationAction>({
phase,
setPhaseData,
errors,
phaseData,
isShowingErrors,
}: React.PropsWithChildren<Props<T>>) => {
const { isLoading, data: nodes, error, resendRequest } = useLoadNodes();
const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState<string | null>(
null
);
if (isLoading) {
return (
<Fragment>
<EuiLoadingSpinner size="xl" />
<EuiSpacer size="m" />
</Fragment>
);
}
if (error) {
const { statusCode, message } = error;
return (
<Fragment>
<EuiCallOut
style={{ maxWidth: 400 }}
title={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle"
defaultMessage="Unable to load node attributes"
/>
}
color="danger"
>
<p>
{message} ({statusCode})
</p>
<EuiButton onClick={resendRequest} iconType="refresh" color="danger">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton"
defaultMessage="Try again"
/>
</EuiButton>
</EuiCallOut>
<EuiSpacer size="xl" />
</Fragment>
);
}
let nodeOptions = Object.keys(nodes).map((attrs) => ({
text: `${attrs} (${nodes[attrs].length})`,
value: attrs,
}));
nodeOptions.sort((a, b) => a.value.localeCompare(b.value));
if (nodeOptions.length) {
nodeOptions = [
{
text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.defaultNodeAllocation', {
defaultMessage: "Default allocation (don't use attributes)",
}),
value: '',
},
...nodeOptions,
];
}
if (!nodeOptions.length) {
return (
<Fragment>
<EuiCallOut
style={{ maxWidth: 400 }}
title={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel"
defaultMessage="No node attributes configured in elasticsearch.yml"
/>
}
color="warning"
>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription"
defaultMessage="You can't control shard allocation without node attributes."
/>
{learnMoreLink}
</EuiCallOut>
<EuiSpacer size="xl" />
</Fragment>
);
}
// check that this string is a valid property
const nodeAttrsProperty = propertyof<T>('selectedNodeAttrs');
return (
<Fragment>
<ErrableFormRow
id={`${phase}-${nodeAttrsProperty}`}
label={i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel', {
defaultMessage: 'Select a node attribute to control shard allocation',
})}
isShowingErrors={isShowingErrors}
errors={errors?.selectedNodeAttrs}
>
<EuiSelect
id={`${phase}-${nodeAttrsProperty}`}
value={phaseData.selectedNodeAttrs || ' '}
options={nodeOptions}
onChange={(e) => {
setPhaseData(nodeAttrsProperty, e.target.value);
}}
/>
</ErrableFormRow>
{!!phaseData.selectedNodeAttrs ? (
<EuiButtonEmpty
style={{ maxWidth: 400 }}
data-test-subj={`${phase}-viewNodeDetailsFlyoutButton`}
flush="left"
iconType="eye"
onClick={() => setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)}
>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.viewNodeDetailsButton"
defaultMessage="View a list of nodes attached to this configuration"
/>
</EuiButtonEmpty>
) : null}
{learnMoreLink}
<EuiSpacer size="m" />
{selectedNodeAttrsForDetails ? (
<NodeAttrsDetails
selectedNodeAttrs={selectedNodeAttrsForDetails}
close={() => setSelectedNodeAttrsForDetails(null)}
/>
) : null}
</Fragment>
);
};

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;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent, useState } from 'react';
import { EuiSpacer, EuiSwitch, EuiSwitchProps } from '@elastic/eui';
export interface Props extends Omit<EuiSwitchProps, 'checked' | 'onChange'> {
initialValue: boolean;
onChange: (nextValue: boolean) => void;
}
export const ToggleableField: FunctionComponent<Props> = ({
initialValue,
onChange,
children,
...restProps
}) => {
const [isContentVisible, setIsContentVisible] = useState<boolean>(initialValue);
return (
<>
<EuiSwitch
{...restProps}
checked={isContentVisible}
onChange={(e) => {
const nextValue = e.target.checked;
setIsContentVisible(nextValue);
onChange(nextValue);
}}
/>
<EuiSpacer size="m" />
{isContentVisible ? children : null}
</>
);
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useEffect, useState } from 'react';
import React, { Fragment, useEffect, useState, useCallback } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
@ -45,7 +45,7 @@ import {
import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components';
import { ColdPhase, DeletePhase, FrozenPhase, HotPhase, WarmPhase } from './phases';
interface Props {
export interface Props {
policies: PolicyFromES[];
policyName: string;
getUrlForApp: (
@ -119,15 +119,39 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout);
};
const setPhaseData = (phase: keyof Phases, key: string, value: any) => {
setPolicy({
...policy,
phases: {
...policy.phases,
[phase]: { ...policy.phases[phase], [key]: value },
},
});
};
const setPhaseData = useCallback(
(phase: keyof Phases, key: string, value: any) => {
setPolicy((nextPolicy) => ({
...nextPolicy,
phases: {
...nextPolicy.phases,
[phase]: { ...nextPolicy.phases[phase], [key]: value },
},
}));
},
[setPolicy]
);
const setHotPhaseData = useCallback(
(key: string, value: any) => setPhaseData('hot', key, value),
[setPhaseData]
);
const setWarmPhaseData = useCallback(
(key: string, value: any) => setPhaseData('warm', key, value),
[setPhaseData]
);
const setColdPhaseData = useCallback(
(key: string, value: any) => setPhaseData('cold', key, value),
[setPhaseData]
);
const setFrozenPhaseData = useCallback(
(key: string, value: any) => setPhaseData('frozen', key, value),
[setPhaseData]
);
const setDeletePhaseData = useCallback(
(key: string, value: any) => setPhaseData('delete', key, value),
[setPhaseData]
);
const setWarmPhaseOnRollover = (value: boolean) => {
setPolicy({
@ -277,7 +301,7 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
<HotPhase
errors={errors?.hot}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.hot).length > 0}
setPhaseData={(key, value) => setPhaseData('hot', key, value)}
setPhaseData={setHotPhaseData}
phaseData={policy.phases.hot}
setWarmPhaseOnRollover={setWarmPhaseOnRollover}
/>
@ -287,7 +311,7 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
<WarmPhase
errors={errors?.warm}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.warm).length > 0}
setPhaseData={(key, value) => setPhaseData('warm', key, value)}
setPhaseData={setWarmPhaseData}
phaseData={policy.phases.warm}
hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled}
/>
@ -297,7 +321,7 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
<ColdPhase
errors={errors?.cold}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.cold).length > 0}
setPhaseData={(key, value) => setPhaseData('cold', key, value)}
setPhaseData={setColdPhaseData}
phaseData={policy.phases.cold}
hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled}
/>
@ -307,7 +331,7 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
<FrozenPhase
errors={errors?.frozen}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.frozen).length > 0}
setPhaseData={(key, value) => setPhaseData('frozen', key, value)}
setPhaseData={setFrozenPhaseData}
phaseData={policy.phases.frozen}
hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled}
/>
@ -318,7 +342,7 @@ export const EditPolicy: React.FunctionComponent<Props> = ({
errors={errors?.delete}
isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.delete).length > 0}
getUrlForApp={getUrlForApp}
setPhaseData={(key, value) => setPhaseData('delete', key, value)}
setPhaseData={setDeletePhaseData}
phaseData={policy.phases.delete}
hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled}
/>

View file

@ -4,19 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { PureComponent, Fragment } from 'react';
import React, { FunctionComponent, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiFieldNumber,
EuiDescribedFormGroup,
EuiSwitch,
EuiTextColor,
} from '@elastic/eui';
import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui';
import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../common/types';
import { PhaseValidationErrors } from '../../../services/policies/policy_validation';
@ -27,14 +19,24 @@ import {
PhaseErrorMessage,
OptionalLabel,
ErrableFormRow,
MinAgeInput,
NodeAllocation,
SetPriorityInput,
MinAgeInput,
DescribedFormField,
} from '../components';
const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', {
defaultMessage: 'Freeze index',
});
import { DataTierAllocationField } from './shared';
const i18nTexts = {
freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', {
defaultMessage: 'Freeze index',
}),
dataTierAllocation: {
description: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.description', {
defaultMessage:
'Move data to data nodes optimized for less frequent, read-only access. Store cold data on less-expensive hardware.',
}),
},
};
const coldProperty: keyof Phases = 'cold';
const phaseProperty = (propertyName: keyof ColdPhaseInterface) => propertyName;
@ -46,18 +48,17 @@ interface Props {
errors?: PhaseValidationErrors<ColdPhaseInterface>;
hotPhaseRolloverEnabled: boolean;
}
export class ColdPhase extends PureComponent<Props> {
render() {
const {
setPhaseData,
phaseData,
errors,
isShowingErrors,
hotPhaseRolloverEnabled,
} = this.props;
return (
<div id="coldPhaseContent" aria-live="polite" role="region">
export const ColdPhase: FunctionComponent<Props> = ({
setPhaseData,
phaseData,
errors,
isShowingErrors,
hotPhaseRolloverEnabled,
}) => {
return (
<div id="coldPhaseContent" aria-live="polite" role="region">
<>
{/* Section title group; containing min age */}
<EuiDescribedFormGroup
title={
<div>
@ -86,7 +87,7 @@ export class ColdPhase extends PureComponent<Props> {
data-test-subj="enablePhaseSwitch-cold"
label={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateWarmPhaseSwitchLabel"
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel"
defaultMessage="Activate cold phase"
/>
}
@ -101,68 +102,83 @@ export class ColdPhase extends PureComponent<Props> {
}
fullWidth
>
<Fragment>
{phaseData.phaseEnabled ? (
<Fragment>
<MinAgeInput<ColdPhaseInterface>
errors={errors}
phaseData={phaseData}
phase={coldProperty}
isShowingErrors={isShowingErrors}
setPhaseData={setPhaseData}
rolloverEnabled={hotPhaseRolloverEnabled}
/>
<EuiSpacer />
<NodeAllocation<ColdPhaseInterface>
phase={coldProperty}
setPhaseData={setPhaseData}
errors={errors}
phaseData={phaseData}
isShowingErrors={isShowingErrors}
/>
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ maxWidth: 188 }}>
<ErrableFormRow
id={`${coldProperty}-${phaseProperty('freezeEnabled')}`}
label={
<Fragment>
<FormattedMessage
id="xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel"
defaultMessage="Number of replicas"
/>
<OptionalLabel />
</Fragment>
}
isShowingErrors={isShowingErrors}
errors={errors?.freezeEnabled}
helpText={i18n.translate(
'xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText',
{
defaultMessage: 'By default, the number of replicas remains the same.',
}
)}
>
<EuiFieldNumber
id={`${coldProperty}-${phaseProperty('selectedReplicaCount')}`}
value={phaseData.selectedReplicaCount}
onChange={(e) => {
setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value);
}}
min={0}
/>
</ErrableFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
) : (
<div />
)}
</Fragment>
{phaseData.phaseEnabled ? (
<MinAgeInput<ColdPhaseInterface>
errors={errors}
phaseData={phaseData}
phase={coldProperty}
isShowingErrors={isShowingErrors}
setPhaseData={setPhaseData}
rolloverEnabled={hotPhaseRolloverEnabled}
/>
) : null}
</EuiDescribedFormGroup>
{phaseData.phaseEnabled ? (
<Fragment>
{/* Data tier allocation section */}
<DataTierAllocationField
description={i18nTexts.dataTierAllocation.description}
phase={coldProperty}
setPhaseData={setPhaseData}
isShowingErrors={isShowingErrors}
phaseData={phaseData}
/>
{/* Replicas section */}
<DescribedFormField
title={
<h3>
{i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', {
defaultMessage: 'Replicas',
})}
</h3>
}
description={i18n.translate(
'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription',
{
defaultMessage:
'Set the number of replicas. Remains the same as the previous phase by default.',
}
)}
switchProps={{
label: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel',
{ defaultMessage: 'Set replicas' }
),
initialValue: Boolean(phaseData.selectedReplicaCount),
onChange: (v) => {
if (!v) {
setPhaseData('selectedReplicaCount', '');
}
},
}}
fullWidth
>
<ErrableFormRow
id={`${coldProperty}-${phaseProperty('selectedReplicaCount')}`}
label={
<Fragment>
<FormattedMessage
id="xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel"
defaultMessage="Number of replicas"
/>
<OptionalLabel />
</Fragment>
}
isShowingErrors={isShowingErrors}
errors={errors?.selectedReplicaCount}
>
<EuiFieldNumber
id={`${coldProperty}-${phaseProperty('selectedReplicaCount')}`}
value={phaseData.selectedReplicaCount}
onChange={(e) => {
setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value);
}}
min={0}
/>
</ErrableFormRow>
</DescribedFormField>
{/* Freeze section */}
<EuiDescribedFormGroup
title={
<h3>
@ -191,8 +207,8 @@ export class ColdPhase extends PureComponent<Props> {
onChange={(e) => {
setPhaseData(phaseProperty('freezeEnabled'), e.target.checked);
}}
label={freezeLabel}
aria-label={freezeLabel}
label={i18nTexts.freezeLabel}
aria-label={i18nTexts.freezeLabel}
/>
</EuiDescribedFormGroup>
<SetPriorityInput<ColdPhaseInterface>
@ -204,7 +220,7 @@ export class ColdPhase extends PureComponent<Props> {
/>
</Fragment>
) : null}
</div>
);
}
}
</>
</div>
);
};

View file

@ -4,19 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { PureComponent, Fragment } from 'react';
import React, { FunctionComponent, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiFieldNumber,
EuiDescribedFormGroup,
EuiSwitch,
EuiTextColor,
} from '@elastic/eui';
import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui';
import { FrozenPhase as FrozenPhaseInterface, Phases } from '../../../../../common/types';
import { PhaseValidationErrors } from '../../../services/policies/policy_validation';
@ -28,13 +20,22 @@ import {
OptionalLabel,
ErrableFormRow,
MinAgeInput,
NodeAllocation,
SetPriorityInput,
DescribedFormField,
} from '../components';
import { DataTierAllocationField } from './shared';
const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', {
defaultMessage: 'Freeze index',
});
const i18nTexts = {
freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', {
defaultMessage: 'Freeze index',
}),
dataTierAllocation: {
description: i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.dataTier.description', {
defaultMessage:
'Move data to data nodes optimized for infrequent, read-only access. Store frozen data on the least-expensive hardware.',
}),
},
};
const frozenProperty: keyof Phases = 'frozen';
const phaseProperty = (propertyName: keyof FrozenPhaseInterface) => propertyName;
@ -46,18 +47,17 @@ interface Props {
errors?: PhaseValidationErrors<FrozenPhaseInterface>;
hotPhaseRolloverEnabled: boolean;
}
export class FrozenPhase extends PureComponent<Props> {
render() {
const {
setPhaseData,
phaseData,
errors,
isShowingErrors,
hotPhaseRolloverEnabled,
} = this.props;
return (
<div id="frozenPhaseContent" aria-live="polite" role="region">
export const FrozenPhase: FunctionComponent<Props> = ({
setPhaseData,
phaseData,
errors,
isShowingErrors,
hotPhaseRolloverEnabled,
}) => {
return (
<div id="frozenPhaseContent" aria-live="polite" role="region">
<>
{/* Section title group; containing min age */}
<EuiDescribedFormGroup
title={
<div>
@ -101,68 +101,82 @@ export class FrozenPhase extends PureComponent<Props> {
}
fullWidth
>
<Fragment>
{phaseData.phaseEnabled ? (
<Fragment>
<MinAgeInput<FrozenPhaseInterface>
errors={errors}
phaseData={phaseData}
phase={frozenProperty}
isShowingErrors={isShowingErrors}
setPhaseData={setPhaseData}
rolloverEnabled={hotPhaseRolloverEnabled}
/>
<EuiSpacer />
<NodeAllocation<FrozenPhaseInterface>
phase={frozenProperty}
setPhaseData={setPhaseData}
errors={errors}
phaseData={phaseData}
isShowingErrors={isShowingErrors}
/>
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ maxWidth: 188 }}>
<ErrableFormRow
id={`${frozenProperty}-${phaseProperty('freezeEnabled')}`}
label={
<Fragment>
<FormattedMessage
id="xpack.indexLifecycleMgmt.frozenPhase.numberOfReplicasLabel"
defaultMessage="Number of replicas"
/>
<OptionalLabel />
</Fragment>
}
isShowingErrors={isShowingErrors}
errors={errors?.freezeEnabled}
helpText={i18n.translate(
'xpack.indexLifecycleMgmt.frozenPhase.replicaCountHelpText',
{
defaultMessage: 'By default, the number of replicas remains the same.',
}
)}
>
<EuiFieldNumber
id={`${frozenProperty}-${phaseProperty('selectedReplicaCount')}`}
value={phaseData.selectedReplicaCount}
onChange={(e) => {
setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value);
}}
min={0}
/>
</ErrableFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
) : (
<div />
)}
</Fragment>
{phaseData.phaseEnabled ? (
<MinAgeInput<FrozenPhaseInterface>
errors={errors}
phaseData={phaseData}
phase={frozenProperty}
isShowingErrors={isShowingErrors}
setPhaseData={setPhaseData}
rolloverEnabled={hotPhaseRolloverEnabled}
/>
) : null}
</EuiDescribedFormGroup>
{phaseData.phaseEnabled ? (
<Fragment>
{/* Data tier allocation section */}
<DataTierAllocationField
description={i18nTexts.dataTierAllocation.description}
phase={frozenProperty}
setPhaseData={setPhaseData}
isShowingErrors={isShowingErrors}
phaseData={phaseData}
/>
{/* Replicas section */}
<DescribedFormField
title={
<h3>
{i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.replicasTitle', {
defaultMessage: 'Replicas',
})}
</h3>
}
description={i18n.translate(
'xpack.indexLifecycleMgmt.frozenPhase.numberOfReplicasDescription',
{
defaultMessage:
'Set the number of replicas. Remains the same as the previous phase by default.',
}
)}
switchProps={{
label: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.numberOfReplicas.switchLabel',
{ defaultMessage: 'Set replicas' }
),
initialValue: Boolean(phaseData.selectedReplicaCount),
onChange: (v) => {
if (!v) {
setPhaseData('selectedReplicaCount', '');
}
},
}}
fullWidth
>
<ErrableFormRow
id={`${frozenProperty}-${phaseProperty('selectedReplicaCount')}`}
label={
<Fragment>
<FormattedMessage
id="xpack.indexLifecycleMgmt.frozenPhase.numberOfReplicasLabel"
defaultMessage="Number of replicas"
/>
<OptionalLabel />
</Fragment>
}
isShowingErrors={isShowingErrors}
errors={errors?.selectedReplicaCount}
>
<EuiFieldNumber
id={`${frozenProperty}-${phaseProperty('selectedReplicaCount')}`}
value={phaseData.selectedReplicaCount}
onChange={(e) => {
setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value);
}}
min={0}
/>
</ErrableFormRow>
</DescribedFormField>
<EuiDescribedFormGroup
title={
<h3>
@ -191,8 +205,8 @@ export class FrozenPhase extends PureComponent<Props> {
onChange={(e) => {
setPhaseData(phaseProperty('freezeEnabled'), e.target.checked);
}}
label={freezeLabel}
aria-label={freezeLabel}
label={i18nTexts.freezeLabel}
aria-label={i18nTexts.freezeLabel}
/>
</EuiDescribedFormGroup>
<SetPriorityInput<FrozenPhaseInterface>
@ -204,7 +218,7 @@ export class FrozenPhase extends PureComponent<Props> {
/>
</Fragment>
) : null}
</div>
);
}
}
</>
</div>
);
};

View file

@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui';
import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../common/types';
import {
DataTierAllocation,
DefaultAllocationWarning,
NoNodeAttributesWarning,
NodesDataProvider,
} from '../../components/data_tier_allocation';
import { PhaseValidationErrors } from '../../../../services/policies/policy_validation';
import { isPhaseDefaultDataAllocationCompatible } from '../../../../lib/data_tiers';
const i18nTexts = {
title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', {
defaultMessage: 'Data allocation',
}),
};
interface Props {
description: React.ReactNode;
phase: PhaseWithAllocation;
setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void;
isShowingErrors: boolean;
errors?: PhaseValidationErrors<PhaseWithAllocationAction>;
phaseData: PhaseWithAllocationAction;
}
/**
* Top-level layout control for the data tier allocation field.
*/
export const DataTierAllocationField: FunctionComponent<Props> = ({
description,
phase,
phaseData,
setPhaseData,
isShowingErrors,
errors,
}) => {
return (
<NodesDataProvider>
{(nodesData) => {
const isCompatible = isPhaseDefaultDataAllocationCompatible(phase, nodesData.nodesByRoles);
const hasNodeAttrs = Boolean(Object.keys(nodesData.nodesByAttributes ?? {}).length);
return (
<EuiDescribedFormGroup
title={<h3>{i18nTexts.title}</h3>}
description={description}
fullWidth
>
<EuiFormRow>
<>
<DataTierAllocation
hasNodeAttributes={hasNodeAttrs}
phase={phase}
errors={errors}
setPhaseData={setPhaseData}
phaseData={phaseData}
isShowingErrors={isShowingErrors}
nodes={nodesData.nodesByAttributes}
/>
{/* Data tier related warnings */}
{phaseData.dataTierAllocationType === 'default' && !isCompatible && (
<DefaultAllocationWarning phase={phase} />
)}
{phaseData.dataTierAllocationType === 'custom' && !hasNodeAttrs && (
<NoNodeAttributesWarning phase={phase} />
)}
</>
</EuiFormRow>
</EuiDescribedFormGroup>
);
}}
</NodesDataProvider>
);
};

View file

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

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, PureComponent } from 'react';
import React, { Fragment, FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
@ -27,44 +27,53 @@ import {
OptionalLabel,
ErrableFormRow,
SetPriorityInput,
NodeAllocation,
MinAgeInput,
DescribedFormField,
Forcemerge,
} from '../components';
import { DataTierAllocationField } from './shared';
const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', {
defaultMessage: 'Shrink index',
});
const moveToWarmPhaseOnRolloverLabel = i18n.translate(
'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel',
{
defaultMessage: 'Move to warm phase on rollover',
}
);
const i18nTexts = {
shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', {
defaultMessage: 'Shrink index',
}),
moveToWarmPhaseOnRolloverLabel: i18n.translate(
'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel',
{
defaultMessage: 'Move to warm phase on rollover',
}
),
dataTierAllocation: {
description: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.description', {
defaultMessage:
'Move warm data to nodes optimized for read-only access. Store warm data on less-expensive hardware.',
}),
},
};
const warmProperty: keyof Phases = 'warm';
const phaseProperty = (propertyName: keyof WarmPhaseInterface) => propertyName;
interface Props {
setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void;
setPhaseData: (
key: keyof WarmPhaseInterface & string,
value: boolean | string | undefined
) => void;
phaseData: WarmPhaseInterface;
isShowingErrors: boolean;
errors?: PhaseValidationErrors<WarmPhaseInterface>;
hotPhaseRolloverEnabled: boolean;
}
export class WarmPhase extends PureComponent<Props> {
render() {
const {
setPhaseData,
phaseData,
errors,
isShowingErrors,
hotPhaseRolloverEnabled,
} = this.props;
return (
<div id="warmPhaseContent" aria-live="polite" role="region" aria-relevant="additions">
export const WarmPhase: FunctionComponent<Props> = ({
setPhaseData,
phaseData,
errors,
isShowingErrors,
hotPhaseRolloverEnabled,
}) => {
return (
<div id="warmPhaseContent" aria-live="polite" role="region" aria-relevant="additions">
<>
<EuiDescribedFormGroup
title={
<div>
@ -115,7 +124,7 @@ export class WarmPhase extends PureComponent<Props> {
<EuiFormRow id={`${warmProperty}-${phaseProperty('warmPhaseOnRollover')}`}>
<EuiSwitch
data-test-subj="warmPhaseOnRolloverSwitch"
label={moveToWarmPhaseOnRolloverLabel}
label={i18nTexts.moveToWarmPhaseOnRolloverLabel}
id={`${warmProperty}-${phaseProperty('warmPhaseOnRollover')}`}
checked={phaseData.warmPhaseOnRollover}
onChange={(e) => {
@ -137,58 +146,75 @@ export class WarmPhase extends PureComponent<Props> {
/>
</Fragment>
) : null}
<EuiSpacer />
<NodeAllocation<WarmPhaseInterface>
phase={warmProperty}
setPhaseData={setPhaseData}
errors={errors}
phaseData={phaseData}
isShowingErrors={isShowingErrors}
/>
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ maxWidth: 188 }}>
<ErrableFormRow
id={`${warmProperty}-${phaseProperty('selectedReplicaCount')}`}
label={
<Fragment>
<FormattedMessage
id="xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel"
defaultMessage="Number of replicas"
/>
<OptionalLabel />
</Fragment>
}
isShowingErrors={isShowingErrors}
errors={errors?.selectedReplicaCount}
helpText={i18n.translate(
'xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText',
{
defaultMessage: 'By default, the number of replicas remains the same.',
}
)}
>
<EuiFieldNumber
id={`${warmProperty}-${phaseProperty('selectedReplicaCount')}`}
value={phaseData.selectedReplicaCount}
onChange={(e) => {
setPhaseData('selectedReplicaCount', e.target.value);
}}
min={0}
/>
</ErrableFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
</Fragment>
) : null}
</Fragment>
</EuiDescribedFormGroup>
{phaseData.phaseEnabled ? (
<Fragment>
{/* Data tier allocation section */}
<DataTierAllocationField
description={i18nTexts.dataTierAllocation.description}
phase={warmProperty}
setPhaseData={setPhaseData}
isShowingErrors={isShowingErrors}
phaseData={phaseData}
/>
<DescribedFormField
title={
<h3>
{i18n.translate('xpack.indexLifecycleMgmt.warmPhase.replicasTitle', {
defaultMessage: 'Replicas',
})}
</h3>
}
description={i18n.translate(
'xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription',
{
defaultMessage:
'Set the number of replicas. Remains the same as the previous phase by default.',
}
)}
switchProps={{
label: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel',
{ defaultMessage: 'Set replicas' }
),
initialValue: Boolean(phaseData.selectedReplicaCount),
onChange: (v) => {
if (!v) {
setPhaseData('selectedReplicaCount', '');
}
},
}}
fullWidth
>
<ErrableFormRow
id={`${warmProperty}-${phaseProperty('selectedReplicaCount')}`}
label={
<Fragment>
<FormattedMessage
id="xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel"
defaultMessage="Number of replicas"
/>
<OptionalLabel />
</Fragment>
}
isShowingErrors={isShowingErrors}
errors={errors?.selectedReplicaCount}
>
<EuiFieldNumber
id={`${warmProperty}-${phaseProperty('selectedReplicaCount')}`}
value={phaseData.selectedReplicaCount}
onChange={(e) => {
setPhaseData('selectedReplicaCount', e.target.value);
}}
min={0}
/>
</ErrableFormRow>
</DescribedFormField>
<EuiDescribedFormGroup
title={
<h3>
@ -217,8 +243,8 @@ export class WarmPhase extends PureComponent<Props> {
onChange={(e) => {
setPhaseData(phaseProperty('shrinkEnabled'), e.target.checked);
}}
label={shrinkLabel}
aria-label={shrinkLabel}
label={i18nTexts.shrinkLabel}
aria-label={i18nTexts.shrinkLabel}
aria-controls="shrinkContent"
/>
@ -275,7 +301,7 @@ export class WarmPhase extends PureComponent<Props> {
/>
</Fragment>
) : null}
</div>
);
}
}
</>
</div>
);
};

View file

@ -6,7 +6,7 @@
import { METRIC_TYPE } from '@kbn/analytics';
import { PolicyFromES, SerializedPolicy } from '../../../common/types';
import { PolicyFromES, SerializedPolicy, ListNodesRouteResponse } from '../../../common/types';
import {
UIM_POLICY_DELETE,
@ -23,10 +23,10 @@ interface GenericObject {
}
export const useLoadNodes = () => {
return useRequest({
return useRequest<ListNodesRouteResponse>({
path: `nodes/list`,
method: 'get',
initialData: [],
initialData: { nodesByAttributes: {}, nodesByRoles: {} } as ListNodesRouteResponse,
});
};

View file

@ -13,6 +13,8 @@ import {
PhaseValidationErrors,
positiveNumberRequiredMessage,
} from './policy_validation';
import { determineDataTierAllocationType } from '../../lib';
import { serializePhaseWithAllocation } from './shared';
const coldPhaseInitialization: ColdPhase = {
phaseEnabled: false,
@ -22,6 +24,7 @@ const coldPhaseInitialization: ColdPhase = {
selectedReplicaCount: '',
freezeEnabled: false,
phaseIndexPriority: '',
dataTierAllocationType: 'default',
};
export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhase => {
@ -32,6 +35,12 @@ export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhas
phase.phaseEnabled = true;
if (phaseSerialized.actions.allocate) {
phase.dataTierAllocationType = determineDataTierAllocationType(
phaseSerialized.actions.allocate
);
}
if (phaseSerialized.min_age) {
const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age);
phase.selectedMinimumAge = minAge;
@ -80,19 +89,7 @@ export const coldPhaseToES = (
esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`;
}
esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {};
if (phase.selectedNodeAttrs) {
const [name, value] = phase.selectedNodeAttrs.split(':');
esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction);
esPhase.actions.allocate.require = {
[name]: value,
};
} else {
if (esPhase.actions.allocate) {
delete esPhase.actions.allocate.require;
}
}
esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions);
if (isNumber(phase.selectedReplicaCount)) {
esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction);

View file

@ -13,6 +13,8 @@ import {
PhaseValidationErrors,
positiveNumberRequiredMessage,
} from './policy_validation';
import { determineDataTierAllocationType } from '../../lib';
import { serializePhaseWithAllocation } from './shared';
const frozenPhaseInitialization: FrozenPhase = {
phaseEnabled: false,
@ -22,6 +24,7 @@ const frozenPhaseInitialization: FrozenPhase = {
selectedReplicaCount: '',
freezeEnabled: false,
phaseIndexPriority: '',
dataTierAllocationType: 'default',
};
export const frozenPhaseFromES = (phaseSerialized?: SerializedFrozenPhase): FrozenPhase => {
@ -32,6 +35,12 @@ export const frozenPhaseFromES = (phaseSerialized?: SerializedFrozenPhase): Froz
phase.phaseEnabled = true;
if (phaseSerialized.actions.allocate) {
phase.dataTierAllocationType = determineDataTierAllocationType(
phaseSerialized.actions.allocate
);
}
if (phaseSerialized.min_age) {
const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age);
phase.selectedMinimumAge = minAge;
@ -80,19 +89,7 @@ export const frozenPhaseToES = (
esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`;
}
esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {};
if (phase.selectedNodeAttrs) {
const [name, value] = phase.selectedNodeAttrs.split(':');
esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction);
esPhase.actions.allocate.require = {
[name]: value,
};
} else {
if (esPhase.actions.allocate) {
delete esPhase.actions.allocate.require;
}
}
esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions);
if (isNumber(phase.selectedReplicaCount)) {
esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction);

View file

@ -0,0 +1,464 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import cloneDeep from 'lodash/cloneDeep';
import { serializePolicy } from './policy_serialization';
import {
defaultNewColdPhase,
defaultNewDeletePhase,
defaultNewFrozenPhase,
defaultNewHotPhase,
defaultNewWarmPhase,
} from '../../constants';
import { DataTierAllocationType } from '../../../../common/types';
describe('Policy serialization', () => {
test('serialize a policy using "default" data allocation', () => {
expect(
serializePolicy(
{
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'default',
// These selected attrs should be ignored
selectedNodeAttrs: 'another:thing',
phaseEnabled: true,
},
cold: {
...defaultNewColdPhase,
dataTierAllocationType: 'default',
selectedNodeAttrs: 'another:thing',
phaseEnabled: true,
},
frozen: {
...defaultNewFrozenPhase,
dataTierAllocationType: 'default',
selectedNodeAttrs: 'another:thing',
phaseEnabled: true,
},
delete: { ...defaultNewDeletePhase },
},
},
{
name: 'test',
phases: {
hot: { actions: {} },
warm: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
cold: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
frozen: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
},
}
)
).toEqual({
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
set_priority: {
priority: 50,
},
},
},
cold: {
actions: {
set_priority: {
priority: 0,
},
},
min_age: '0d',
},
frozen: {
actions: {
set_priority: {
priority: 0,
},
},
min_age: '0d',
},
},
});
});
test('serialize a policy using "custom" data allocation', () => {
expect(
serializePolicy(
{
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'custom',
selectedNodeAttrs: 'another:thing',
phaseEnabled: true,
},
cold: {
...defaultNewColdPhase,
dataTierAllocationType: 'custom',
selectedNodeAttrs: 'another:thing',
phaseEnabled: true,
},
frozen: {
...defaultNewFrozenPhase,
dataTierAllocationType: 'custom',
selectedNodeAttrs: 'another:thing',
phaseEnabled: true,
},
delete: { ...defaultNewDeletePhase },
},
},
{
name: 'test',
phases: {
hot: { actions: {} },
warm: {
actions: {
allocate: {
include: { keep: 'this' },
exclude: { keep: 'this' },
require: { something: 'here' },
},
},
},
cold: {
actions: {
allocate: {
include: { keep: 'this' },
exclude: { keep: 'this' },
require: { something: 'here' },
},
},
},
frozen: {
actions: {
allocate: {
include: { keep: 'this' },
exclude: { keep: 'this' },
require: { something: 'here' },
},
},
},
},
}
)
).toEqual({
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
allocate: {
include: { keep: 'this' },
exclude: { keep: 'this' },
require: {
another: 'thing',
},
},
set_priority: {
priority: 50,
},
},
},
cold: {
actions: {
allocate: {
include: { keep: 'this' },
exclude: { keep: 'this' },
require: {
another: 'thing',
},
},
set_priority: {
priority: 0,
},
},
min_age: '0d',
},
frozen: {
actions: {
allocate: {
include: { keep: 'this' },
exclude: { keep: 'this' },
require: {
another: 'thing',
},
},
set_priority: {
priority: 0,
},
},
min_age: '0d',
},
},
});
});
test('serialize a policy using "custom" data allocation with no node attributes', () => {
expect(
serializePolicy(
{
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'custom',
selectedNodeAttrs: '',
phaseEnabled: true,
},
cold: {
...defaultNewColdPhase,
dataTierAllocationType: 'custom',
selectedNodeAttrs: '',
phaseEnabled: true,
},
frozen: {
...defaultNewFrozenPhase,
dataTierAllocationType: 'custom',
selectedNodeAttrs: '',
phaseEnabled: true,
},
delete: { ...defaultNewDeletePhase },
},
},
{
name: 'test',
phases: {
hot: { actions: {} },
warm: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
cold: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
frozen: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
},
}
)
).toEqual({
// There should be no allocation action in any phases...
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
allocate: { include: {}, exclude: {}, require: { something: 'here' } },
set_priority: {
priority: 50,
},
},
},
cold: {
actions: {
allocate: { include: {}, exclude: {}, require: { something: 'here' } },
set_priority: {
priority: 0,
},
},
min_age: '0d',
},
frozen: {
actions: {
allocate: { include: {}, exclude: {}, require: { something: 'here' } },
set_priority: {
priority: 0,
},
},
min_age: '0d',
},
},
});
});
test('serialize a policy using "none" data allocation with no node attributes', () => {
expect(
serializePolicy(
{
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'none',
selectedNodeAttrs: 'ignore:this',
phaseEnabled: true,
},
cold: {
...defaultNewColdPhase,
dataTierAllocationType: 'none',
selectedNodeAttrs: 'ignore:this',
phaseEnabled: true,
},
frozen: {
...defaultNewFrozenPhase,
dataTierAllocationType: 'none',
selectedNodeAttrs: 'ignore:this',
phaseEnabled: true,
},
delete: { ...defaultNewDeletePhase },
},
},
{
name: 'test',
phases: {
hot: { actions: {} },
warm: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
cold: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
frozen: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
},
}
)
).toEqual({
// There should be no allocation action in any phases...
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_size: '50gb',
},
set_priority: {
priority: 100,
},
},
},
warm: {
actions: {
migrate: {
enabled: false,
},
set_priority: {
priority: 50,
},
},
},
cold: {
actions: {
migrate: {
enabled: false,
},
set_priority: {
priority: 0,
},
},
min_age: '0d',
},
frozen: {
actions: {
migrate: {
enabled: false,
},
set_priority: {
priority: 0,
},
},
min_age: '0d',
},
},
});
});
test('serialization does not alter the original policy', () => {
const originalPolicy = {
name: 'test',
phases: {
hot: { actions: {} },
warm: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
cold: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
frozen: {
actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } },
},
},
};
const originalClone = cloneDeep(originalPolicy);
const deserializedPolicy = {
name: 'test',
phases: {
hot: { ...defaultNewHotPhase },
warm: {
...defaultNewWarmPhase,
dataTierAllocationType: 'none' as DataTierAllocationType,
selectedNodeAttrs: 'ignore:this',
phaseEnabled: true,
},
cold: {
...defaultNewColdPhase,
dataTierAllocationType: 'none' as DataTierAllocationType,
selectedNodeAttrs: 'ignore:this',
phaseEnabled: true,
},
frozen: {
...defaultNewFrozenPhase,
dataTierAllocationType: 'none' as DataTierAllocationType,
selectedNodeAttrs: 'ignore:this',
phaseEnabled: true,
},
delete: { ...defaultNewDeletePhase },
},
};
serializePolicy(deserializedPolicy, originalPolicy);
deserializedPolicy.phases.warm.dataTierAllocationType = 'custom';
serializePolicy(deserializedPolicy, originalPolicy);
deserializedPolicy.phases.warm.dataTierAllocationType = 'default';
serializePolicy(deserializedPolicy, originalPolicy);
expect(originalPolicy).toEqual(originalClone);
});
});

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { serializePhaseWithAllocation } from './serialize_phase_with_allocation';

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import cloneDeep from 'lodash/cloneDeep';
import {
AllocateAction,
PhaseWithAllocationAction,
SerializedPhase,
} from '../../../../../common/types';
export const serializePhaseWithAllocation = (
phase: PhaseWithAllocationAction,
originalPhaseActions: SerializedPhase['actions'] = {}
): SerializedPhase['actions'] => {
const esPhaseActions: SerializedPhase['actions'] = cloneDeep(originalPhaseActions);
if (phase.dataTierAllocationType === 'custom' && phase.selectedNodeAttrs) {
const [name, value] = phase.selectedNodeAttrs.split(':');
esPhaseActions.allocate = esPhaseActions.allocate || ({} as AllocateAction);
esPhaseActions.allocate.require = {
[name]: value,
};
} else if (phase.dataTierAllocationType === 'none') {
esPhaseActions.migrate = { enabled: false };
if (esPhaseActions.allocate) {
delete esPhaseActions.allocate;
}
} else if (phase.dataTierAllocationType === 'default') {
if (esPhaseActions.allocate) {
delete esPhaseActions.allocate.require;
}
delete esPhaseActions.migrate;
}
return esPhaseActions;
};

View file

@ -16,6 +16,9 @@ import {
positiveNumbersAboveZeroErrorMessage,
} from './policy_validation';
import { determineDataTierAllocationType } from '../../lib';
import { serializePhaseWithAllocation } from './shared';
const warmPhaseInitialization: WarmPhase = {
phaseEnabled: false,
warmPhaseOnRollover: false,
@ -28,6 +31,7 @@ const warmPhaseInitialization: WarmPhase = {
forceMergeEnabled: false,
selectedForceMergeSegments: '',
phaseIndexPriority: '',
dataTierAllocationType: 'default',
};
export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhase => {
@ -39,6 +43,12 @@ export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhas
phase.phaseEnabled = true;
if (phaseSerialized.actions.allocate) {
phase.dataTierAllocationType = determineDataTierAllocationType(
phaseSerialized.actions.allocate
);
}
if (phaseSerialized.min_age) {
if (phaseSerialized.min_age === '0ms') {
phase.warmPhaseOnRollover = true;
@ -99,19 +109,7 @@ export const warmPhaseToES = (
delete esPhase.min_age;
}
esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {};
if (phase.selectedNodeAttrs) {
const [name, value] = phase.selectedNodeAttrs.split(':');
esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction);
esPhase.actions.allocate.require = {
[name]: value,
};
} else {
if (esPhase.actions.allocate) {
delete esPhase.actions.allocate.require;
}
}
esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions);
if (isNumber(phase.selectedReplicaCount)) {
esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction);

View file

@ -6,22 +6,45 @@
import { LegacyAPICaller } from 'src/core/server';
import { ListNodesRouteResponse, NodeDataRole } from '../../../../common/types';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../../../services';
function convertStatsIntoList(stats: any, disallowedNodeAttributes: string[]): any {
return Object.entries(stats.nodes).reduce((accum: any, [nodeId, nodeStats]: [any, any]) => {
const attributes = nodeStats.attributes || {};
for (const [key, value] of Object.entries(attributes)) {
const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key);
if (isNodeAttributeAllowed) {
const attributeString = `${key}:${value}`;
accum[attributeString] = accum[attributeString] || [];
accum[attributeString].push(nodeId);
interface Stats {
nodes: {
[nodeId: string]: {
attributes: Record<string, string>;
roles: string[];
};
};
}
function convertStatsIntoList(
stats: Stats,
disallowedNodeAttributes: string[]
): ListNodesRouteResponse {
return Object.entries(stats.nodes).reduce(
(accum, [nodeId, nodeStats]) => {
const attributes = nodeStats.attributes || {};
for (const [key, value] of Object.entries(attributes)) {
const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key);
if (isNodeAttributeAllowed) {
const attributeString = `${key}:${value}`;
accum.nodesByAttributes[attributeString] = accum.nodesByAttributes[attributeString] ?? [];
accum.nodesByAttributes[attributeString].push(nodeId);
}
}
}
return accum;
}, {});
const dataRoles = nodeStats.roles.filter((r) => r.startsWith('data')) as NodeDataRole[];
for (const role of dataRoles) {
accum.nodesByRoles[role as NodeDataRole] = accum.nodesByRoles[role] ?? [];
accum.nodesByRoles[role as NodeDataRole]!.push(nodeId);
}
return accum;
},
{ nodesByAttributes: {}, nodesByRoles: {} } as ListNodesRouteResponse
);
}
async function fetchNodeStats(callAsCurrentUser: LegacyAPICaller): Promise<any> {
@ -54,8 +77,8 @@ export function registerListRoute({ router, config, license, lib }: RouteDepende
const stats = await fetchNodeStats(
context.core.elasticsearch.legacy.client.callAsCurrentUser
);
const okResponse = { body: convertStatsIntoList(stats, disallowedNodeAttributes) };
return response.ok(okResponse);
const body: ListNodesRouteResponse = convertStatsIntoList(stats, disallowedNodeAttributes);
return response.ok({ body });
} catch (e) {
if (lib.isEsError(e)) {
return response.customError({

View file

@ -40,6 +40,8 @@ const setPrioritySchema = schema.maybe(
const unfollowSchema = schema.maybe(schema.object({})); // Unfollow has no options
const migrateSchema = schema.maybe(schema.object({ enabled: schema.literal(false) }));
const allocateNodeSchema = schema.maybe(schema.recordOf(schema.string(), schema.string()));
const allocateSchema = schema.maybe(
schema.object({
@ -76,6 +78,7 @@ const warmPhaseSchema = schema.maybe(
schema.object({
min_age: minAgeSchema,
actions: schema.object({
migrate: migrateSchema,
set_priority: setPrioritySchema,
unfollow: unfollowSchema,
readonly: schema.maybe(schema.object({})), // Readonly has no options
@ -94,6 +97,7 @@ const coldPhaseSchema = schema.maybe(
schema.object({
min_age: minAgeSchema,
actions: schema.object({
migrate: migrateSchema,
set_priority: setPrioritySchema,
unfollow: unfollowSchema,
allocate: allocateSchema,
@ -111,6 +115,7 @@ const frozenPhaseSchema = schema.maybe(
schema.object({
min_age: minAgeSchema,
actions: schema.object({
migrate: migrateSchema,
set_priority: setPrioritySchema,
unfollow: unfollowSchema,
allocate: allocateSchema,

View file

@ -7978,7 +7978,6 @@
"xpack.indexLifecycleMgmt.appTitle": "インデックスライフサイクルポリシー",
"xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結",
"xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "複製の数",
"xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText": "デフォルトで、複製の数は同じままになります。",
"xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "キャンセル",
"xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "削除",
"xpack.indexLifecycleMgmt.confirmDelete.errorMessage": "ポリシー {policyName} の削除中にエラーが発生しました",
@ -7986,7 +7985,6 @@
"xpack.indexLifecycleMgmt.confirmDelete.title": "ポリシー「{name}」が削除されました",
"xpack.indexLifecycleMgmt.confirmDelete.undoneWarning": "削除されたポリシーは復元できません。",
"xpack.indexLifecycleMgmt.editPolicy.cancelButton": "キャンセル",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateWarmPhaseSwitchLabel": "コールドフェーズを有効にする",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "インデックスへのクエリの頻度を減らすことで、大幅に性能が低いハードウェアにシャードを割り当てることができます。クエリが遅いため、複製の数を減らすことができます。",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "コールドフェーズ",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "凍結されたインデックスはクラスターにほとんどオーバーヘッドがなく、書き込みオペレーションがブロックされます。凍結されたインデックスは検索できますが、クエリが遅くなります。",
@ -8032,7 +8030,6 @@
"xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大インデックスサイズが必要です。",
"xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名前",
"xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "シャードの割当をコントロールするノード属性を選択",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "ノード属性なしではシャードの割り当てをコントロールできません。",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml でノード属性が構成されていません",
"xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字が必要です。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "コールドフェーズのタイミング",
@ -8188,7 +8185,6 @@
"xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "プライマリシャードの数",
"xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "レプリカの数",
"xpack.indexLifecycleMgmt.warmPhase.numberOfSegmentsLabel": "セグメントの数",
"xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText": "デフォルトで、レプリカの数は同じままになります。",
"xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "インデックスを縮小",
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)",
"xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件",

View file

@ -7982,7 +7982,6 @@
"xpack.indexLifecycleMgmt.appTitle": "索引生命周期策略",
"xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引",
"xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "副本分片数目",
"xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText": "默认情况下,副本分片数目仍一样。",
"xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "取消",
"xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "删除",
"xpack.indexLifecycleMgmt.confirmDelete.errorMessage": "删除策略 {policyName} 时出错",
@ -7990,7 +7989,6 @@
"xpack.indexLifecycleMgmt.confirmDelete.title": "删除策略“{name}”",
"xpack.indexLifecycleMgmt.confirmDelete.undoneWarning": "无法恢复删除的策略。",
"xpack.indexLifecycleMgmt.editPolicy.cancelButton": "取消",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateWarmPhaseSwitchLabel": "激活冷阶段",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "您查询自己索引的频率较低,因此您可以在效率较低的硬件上分配分片。因为您的查询较为缓慢,所以您可以减少副本分片数目。",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "冷阶段",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "冻结的索引在集群上有很少的开销,已被阻止进行写操作。您可以搜索冻结的索引,但查询应会较慢。",
@ -8036,7 +8034,6 @@
"xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大索引大小必填。",
"xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名称",
"xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "选择节点属性来控制分片分配",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "没有节点属性,将无法控制分片分配。",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml 中未配置任何节点属性",
"xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字必填。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "冷阶段计时",
@ -8192,7 +8189,6 @@
"xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "主分片数目",
"xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "副本分片数目",
"xpack.indexLifecycleMgmt.warmPhase.numberOfSegmentsLabel": "段数目",
"xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText": "默认情况下,副本分片数目仍一样。",
"xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "缩小索引",
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)",
"xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据",

View file

@ -29,7 +29,7 @@ export default function ({ getService }) {
const nodesIds = Object.keys(nodeStats.nodes);
const { body } = await loadNodes().expect(200);
expect(body[NODE_CUSTOM_ATTRIBUTE]).to.eql(nodesIds);
expect(body.nodesByAttributes[NODE_CUSTOM_ATTRIBUTE]).to.eql(nodesIds);
});
});