[ILM] Add support for frozen phase (#93068)

This commit is contained in:
Sébastien Loix 2021-03-10 20:06:11 +00:00 committed by GitHub
parent a198744553
commit a632f3f59f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 759 additions and 356 deletions

View file

@ -208,8 +208,8 @@ export const setup = async (arg?: {
};
};
const setFreeze = createFormToggleAction('freezeSwitch');
const freezeExists = () => exists('freezeSwitch');
const createSetFreeze = (phase: Phases) => createFormToggleAction(`${phase}-freezeSwitch`);
const createFreezeExists = (phase: Phases) => () => exists(`${phase}-freezeSwitch`);
const createReadonlyActions = (phase: Phases) => {
const toggleSelector = `${phase}-readonlySwitch`;
@ -275,21 +275,21 @@ export const setup = async (arg?: {
const dataTierSelector = `${controlsSelector}.dataTierSelect`;
const nodeAttrsSelector = `${phase}-selectedNodeAttrs`;
const openNodeAttributesSection = async () => {
await act(async () => {
find(dataTierSelector).simulate('click');
});
component.update();
};
return {
hasDataTierAllocationControls: () => exists(controlsSelector),
openNodeAttributesSection: async () => {
await act(async () => {
find(dataTierSelector).simulate('click');
});
component.update();
},
openNodeAttributesSection,
hasNodeAttributesSelect: (): boolean => exists(nodeAttrsSelector),
getNodeAttributesSelectOptions: () => find(nodeAttrsSelector).find('option'),
setDataAllocation: async (value: DataTierAllocationType) => {
act(() => {
find(dataTierSelector).simulate('click');
});
component.update();
await openNodeAttributesSection();
await act(async () => {
switch (value) {
case 'node_roles':
@ -359,6 +359,7 @@ export const setup = async (arg?: {
hasHotPhase: () => exists('ilmTimelineHotPhase'),
hasWarmPhase: () => exists('ilmTimelineWarmPhase'),
hasColdPhase: () => exists('ilmTimelineColdPhase'),
hasFrozenPhase: () => exists('ilmTimelineFrozenPhase'),
hasDeletePhase: () => exists('ilmTimelineDeletePhase'),
},
hot: {
@ -390,13 +391,19 @@ export const setup = async (arg?: {
enable: enable('cold'),
...createMinAgeActions('cold'),
setReplicas: setReplicas('cold'),
setFreeze,
freezeExists,
setFreeze: createSetFreeze('cold'),
freezeExists: createFreezeExists('cold'),
hasErrorIndicator: () => exists('phaseErrorIndicator-cold'),
...createIndexPriorityActions('cold'),
...createSearchableSnapshotActions('cold'),
...createNodeAllocationActions('cold'),
},
frozen: {
enable: enable('frozen'),
...createMinAgeActions('frozen'),
hasErrorIndicator: () => exists('phaseErrorIndicator-frozen'),
...createSearchableSnapshotActions('frozen'),
},
delete: {
isShown: () => exists('delete-phaseContent'),
...createToggleDeletePhaseActions(),

View file

@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { act } from 'react-dom/test-utils';
import { licensingMock } from '../../../../../licensing/public/mocks';
import { setupEnvironment } from '../../helpers/setup_environment';
import { EditPolicyTestBed, setup } from '../edit_policy.helpers';
import { getDefaultHotPhasePolicy } from '../constants';
describe('<EditPolicy /> frozen phase', () => {
let testBed: EditPolicyTestBed;
const { server, httpRequestsMockHelpers } = setupEnvironment();
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
server.restore();
});
beforeEach(async () => {
httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
httpRequestsMockHelpers.setListNodes({
nodesByRoles: { data: ['node1'] },
nodesByAttributes: { 'attribute:true': ['node1'] },
isUsingDeprecatedDataRoleConfig: true,
});
httpRequestsMockHelpers.setNodesDetails('attribute:true', [
{ nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } },
]);
httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
await act(async () => {
testBed = await setup();
});
const { component } = testBed;
component.update();
});
test('shows timing only when enabled', async () => {
const { actions, exists } = testBed;
expect(exists('frozen-phase')).toBe(true);
expect(actions.frozen.hasMinAgeInput()).toBeFalsy();
await actions.frozen.enable(true);
expect(actions.frozen.hasMinAgeInput()).toBeTruthy();
});
describe('on non-enterprise license', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
httpRequestsMockHelpers.setListNodes({
isUsingDeprecatedDataRoleConfig: false,
nodesByAttributes: { test: ['123'] },
nodesByRoles: { data: ['123'] },
});
httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['my-repo'] });
await act(async () => {
testBed = await setup({
appServicesContext: {
license: licensingMock.createLicense({ license: { type: 'basic' } }),
},
});
});
const { component } = testBed;
component.update();
});
test('should not be available', async () => {
const { exists } = testBed;
expect(exists('frozen-phase')).toBe(false);
});
});
});

View file

@ -216,6 +216,7 @@ describe('<EditPolicy /> node allocation', () => {
test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => {
const { actions, component } = testBed;
await actions.cold.enable(true);
expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy();
@ -250,23 +251,37 @@ describe('<EditPolicy /> node allocation', () => {
expect(actions.cold.hasDefaultAllocationWarning()).toBeTruthy();
});
test('shows default allocation notice when warm or hot tiers exists, but not cold tier', async () => {
httpRequestsMockHelpers.setListNodes({
nodesByAttributes: {},
[
{
nodesByRoles: { data_hot: ['test'] },
previousActiveRole: 'hot',
},
{
nodesByRoles: { data_hot: ['test'], data_warm: ['test'] },
isUsingDeprecatedDataRoleConfig: false,
previousActiveRole: 'warm',
},
].forEach(({ nodesByRoles, previousActiveRole }) => {
test(`shows default allocation notice when ${previousActiveRole} tiers exists, but not cold tier`, async () => {
httpRequestsMockHelpers.setListNodes({
nodesByAttributes: {},
nodesByRoles,
isUsingDeprecatedDataRoleConfig: false,
});
await act(async () => {
testBed = await setup();
});
const { actions, component, find } = testBed;
component.update();
await actions.cold.enable(true);
expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy();
expect(find('defaultAllocationNotice').text()).toContain(
`This policy will move data in the cold phase to ${previousActiveRole} tier nodes`
);
});
await act(async () => {
testBed = await setup();
});
const { actions, component } = testBed;
component.update();
await actions.cold.enable(true);
expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy();
});
test(`doesn't show default allocation notice when node with "data" role exists`, async () => {
@ -366,7 +381,7 @@ describe('<EditPolicy /> node allocation', () => {
expect(find('cloudDataTierCallout').exists()).toBeFalsy();
});
test('shows cloud notice when cold tier nodes do not exist', async () => {
test(`shows cloud notice when cold tier nodes do not exist`, async () => {
httpRequestsMockHelpers.setListNodes({
nodesByAttributes: {},
nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] },
@ -375,13 +390,17 @@ describe('<EditPolicy /> node allocation', () => {
await act(async () => {
testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } });
});
const { actions, component, exists } = testBed;
const { actions, component, exists, find } = testBed;
component.update();
await actions.cold.enable(true);
expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(exists('cloudMissingColdTierCallout')).toBeTruthy();
expect(exists('cloudMissingTierCallout')).toBeTruthy();
expect(find('cloudMissingTierCallout').text()).toContain(
`Edit your Elastic Cloud deployment to set up a cold tier`
);
// Assert that other notices are not showing
expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy();
expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy();
@ -480,6 +499,7 @@ describe('<EditPolicy /> node allocation', () => {
const { find } = testBed;
expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom');
});
test('detecting use of the "off" allocation type', () => {
const { find } = testBed;
expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off');

View file

@ -45,6 +45,7 @@ describe('<EditPolicy /> timeline', () => {
const { actions } = testBed;
expect(actions.hot.forceMergeFieldExists()).toBeTruthy();
});
test('hides forcemerge when rollover is disabled', async () => {
const { actions } = testBed;
await actions.hot.toggleDefaultRollover(false);
@ -56,22 +57,26 @@ describe('<EditPolicy /> timeline', () => {
const { actions } = testBed;
expect(actions.hot.shrinkExists()).toBeTruthy();
});
test('hides shrink input when rollover is disabled', async () => {
const { actions } = testBed;
await actions.hot.toggleDefaultRollover(false);
await actions.hot.toggleRollover(false);
expect(actions.hot.shrinkExists()).toBeFalsy();
});
test('shows readonly input when rollover enabled', async () => {
const { actions } = testBed;
expect(actions.hot.readonlyExists()).toBeTruthy();
});
test('hides readonly input when rollover is disabled', async () => {
const { actions } = testBed;
await actions.hot.toggleDefaultRollover(false);
await actions.hot.toggleRollover(false);
expect(actions.hot.readonlyExists()).toBeFalsy();
});
test('hides and disables searchable snapshot field', async () => {
const { actions } = testBed;
await actions.hot.toggleDefaultRollover(false);
@ -86,12 +91,15 @@ describe('<EditPolicy /> timeline', () => {
await actions.warm.enable(true);
await actions.cold.enable(true);
await actions.frozen.enable(true);
await actions.delete.enablePhase();
expect(actions.warm.hasRolloverTipOnMinAge()).toBeTruthy();
expect(actions.cold.hasRolloverTipOnMinAge()).toBeTruthy();
expect(actions.frozen.hasRolloverTipOnMinAge()).toBeTruthy();
expect(actions.delete.hasRolloverTipOnMinAge()).toBeTruthy();
});
test('hiding rollover tip on minimum age', async () => {
const { actions } = testBed;
await actions.hot.toggleDefaultRollover(false);
@ -99,10 +107,12 @@ describe('<EditPolicy /> timeline', () => {
await actions.warm.enable(true);
await actions.cold.enable(true);
await actions.frozen.enable(true);
await actions.delete.enablePhase();
expect(actions.warm.hasRolloverTipOnMinAge()).toBeFalsy();
expect(actions.cold.hasRolloverTipOnMinAge()).toBeFalsy();
expect(actions.frozen.hasRolloverTipOnMinAge()).toBeFalsy();
expect(actions.delete.hasRolloverTipOnMinAge()).toBeFalsy();
});
});

View file

@ -94,6 +94,7 @@ describe('<EditPolicy /> searchable snapshots', () => {
).toBe(true);
});
});
describe('existing policy', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
@ -124,6 +125,7 @@ describe('<EditPolicy /> searchable snapshots', () => {
});
});
});
describe('on non-enterprise license', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]);
@ -145,11 +147,13 @@ describe('<EditPolicy /> searchable snapshots', () => {
const { component } = testBed;
component.update();
});
test('disable setting searchable snapshots', async () => {
const { actions } = testBed;
expect(actions.cold.searchableSnapshotsExists()).toBeFalsy();
expect(actions.hot.searchableSnapshotsExists()).toBeFalsy();
expect(actions.cold.searchableSnapshotsExists()).toBeFalsy();
expect(actions.frozen.searchableSnapshotsExists()).toBeFalsy();
await actions.cold.enable(true);

View file

@ -21,5 +21,6 @@ export type TestSubjects =
| 'policyTablePolicyNameLink'
| 'policyTitle'
| 'createPolicyButton'
| 'freezeSwitch'
| 'cold-freezeSwitch'
| 'frozen-freezeSwitch'
| string;

View file

@ -13,7 +13,15 @@ const WARM_PHASE_NODE_PREFERENCE: DataTierRole[] = ['data_warm', 'data_hot'];
const COLD_PHASE_NODE_PREFERENCE: DataTierRole[] = ['data_cold', 'data_warm', 'data_hot'];
const FROZEN_PHASE_NODE_PREFERENCE: DataTierRole[] = [
'data_frozen',
'data_cold',
'data_warm',
'data_hot',
];
export const phaseToNodePreferenceMap: Record<PhaseWithAllocation, DataTierRole[]> = Object.freeze({
warm: WARM_PHASE_NODE_PREFERENCE,
cold: COLD_PHASE_NODE_PREFERENCE,
frozen: FROZEN_PHASE_NODE_PREFERENCE,
});

View file

@ -12,7 +12,7 @@ export * from './policies';
/**
* These roles reflect how nodes are stratified into different data tiers.
*/
export type DataTierRole = 'data_hot' | 'data_warm' | 'data_cold';
export type DataTierRole = 'data_hot' | 'data_warm' | 'data_cold' | 'data_frozen';
/**
* The "data_content" role can store all data the ES stack uses for feature

View file

@ -7,7 +7,7 @@
import { Index as IndexInterface } from '../../../index_management/common/types';
export type PhaseWithAllocation = 'warm' | 'cold';
export type PhaseWithAllocation = 'warm' | 'cold' | 'frozen';
export interface SerializedPolicy {
name: string;
@ -18,6 +18,7 @@ export interface Phases {
hot?: SerializedHotPhase;
warm?: SerializedWarmPhase;
cold?: SerializedColdPhase;
frozen?: SerializedFrozenPhase;
delete?: SerializedDeletePhase;
}
@ -51,6 +52,8 @@ export interface SerializedActionWithAllocation {
migrate?: MigrateAction;
}
export type SearchableSnapshotStorage = 'full_copy' | 'shared_cache';
export interface SearchableSnapshotAction {
snapshot_repository: string;
/**
@ -58,6 +61,12 @@ export interface SearchableSnapshotAction {
* not suit the vast majority of cases.
*/
force_merge_index?: boolean;
/**
* This configuration lets the user create full or partial searchable snapshots.
* Full searchable snapshots store primary data locally and store replica data in the snapshot.
* Partial searchable snapshots store no data locally.
*/
storage?: SearchableSnapshotStorage;
}
export interface RolloverAction {
@ -111,6 +120,21 @@ export interface SerializedColdPhase extends SerializedPhase {
};
}
export interface SerializedFrozenPhase extends SerializedPhase {
actions: {
freeze?: {};
allocate?: AllocateAction;
set_priority?: {
priority: number | null;
};
migrate?: MigrateAction;
/**
* Only available on enterprise license
*/
searchable_snapshot?: SearchableSnapshotAction;
};
}
export interface SerializedDeletePhase extends SerializedPhase {
actions: {
wait_for_snapshot?: {

View file

@ -11,6 +11,7 @@ export const defaultIndexPriority = {
hot: '100',
warm: '50',
cold: '0',
frozen: '0',
};
export const defaultRolloverAction: RolloverAction = {

View file

@ -23,6 +23,9 @@
&__inner--cold {
fill: $euiColorVis1_behindText;
}
&__inner--frozen {
fill: $euiColorVis4_behindText;
}
&__inner--delete {
fill: $euiColorDarkShade;
}

View file

@ -6,20 +6,15 @@
*/
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiTextColor } from '@elastic/eui';
import { useConfigurationIssues } from '../../../form';
import { LearnMoreLink, ToggleFieldWithDescribedFormRow } from '../../';
import {
DataTierAllocationField,
SearchableSnapshotField,
IndexPriorityField,
ReplicasField,
FreezeField,
} from '../shared_fields';
import { Phase } from '../phase';
@ -41,35 +36,7 @@ export const ColdPhase: FunctionComponent = () => {
<ReplicasField phase="cold" />
{/* Freeze section */}
{!isUsingSearchableSnapshotInHotPhase && (
<ToggleFieldWithDescribedFormRow
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText"
defaultMessage="Freeze"
/>
</h3>
}
description={
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText"
defaultMessage="Make the index read-only and minimize its memory footprint."
/>{' '}
<LearnMoreLink docPath="ilm-freeze.html" />
</EuiTextColor>
}
fullWidth
titleSize="xs"
switchProps={{
'data-test-subj': 'freezeSwitch',
path: '_meta.cold.freezeEnabled',
}}
>
<div />
</ToggleFieldWithDescribedFormRow>
)}
{!isUsingSearchableSnapshotInHotPhase && <FreezeField phase="cold" />}
{/* Data tier allocation section */}
<DataTierAllocationField

View file

@ -72,18 +72,21 @@ export const DeletePhase: FunctionComponent = () => {
);
return (
<EuiComment
data-test-subj="delete-phaseContent"
username={phaseTitle}
actions={<MinAgeField phase={'delete'} />}
className="ilmDeletePhase ilmPhase"
timelineIcon={<PhaseIcon enabled={enabled} phase={'delete'} />}
>
<EuiText color="subdued" size={'s'} style={{ maxWidth: '50%' }}>
{i18nTexts.editPolicy.descriptions.delete}
</EuiText>
<>
<EuiSpacer />
<SnapshotPoliciesField />
</EuiComment>
<EuiComment
data-test-subj="delete-phaseContent"
username={phaseTitle}
actions={<MinAgeField phase={'delete'} />}
className="ilmDeletePhase ilmPhase"
timelineIcon={<PhaseIcon enabled={enabled} phase={'delete'} />}
>
<EuiText color="subdued" size={'s'} style={{ maxWidth: '50%' }}>
{i18nTexts.editPolicy.descriptions.delete}
</EuiText>
<EuiSpacer />
<SnapshotPoliciesField />
</EuiComment>
</>
);
};

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import { SearchableSnapshotField } from '../shared_fields';
import { Phase } from '../phase';
export const FrozenPhase: FunctionComponent = () => {
return (
<Phase
phase="frozen"
topLevelSettings={<SearchableSnapshotField phase="frozen" canBeDisabled={false} />}
/>
);
};

View file

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

View file

@ -11,6 +11,8 @@ export { WarmPhase } from './warm_phase';
export { ColdPhase } from './cold_phase';
export { FrozenPhase } from './frozen_phase';
export { DeletePhase } from './delete_phase';
export { Phase } from './phase';

View file

@ -23,16 +23,13 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { PhasesExceptDelete } from '../../../../../../../common/types';
import { ToggleField, useFormData } from '../../../../../../shared_imports';
import { i18nTexts } from '../../../i18n_texts';
import { FormInternal } from '../../../types';
import { UseField } from '../../../form';
import { PhaseErrorIndicator } from './phase_error_indicator';
import { MinAgeField } from '../shared_fields';
import { PhaseIcon } from '../../phase_icon';
import { PhaseFooter } from '../../phase_footer';
import { PhaseErrorIndicator } from './phase_error_indicator';
import './phase.scss';
interface Props {
@ -99,6 +96,7 @@ export const Phase: FunctionComponent<Props> = ({ children, topLevelSettings, ph
actions={minAge}
timelineIcon={<PhaseIcon enabled={enabled} phase={phase} />}
className={`ilmPhase ${enabled ? 'ilmPhase--enabled' : ''}`}
data-test-subj={`${phase}-phase`}
>
<EuiText color="subdued" size={'s'} style={{ maxWidth: '50%' }}>
{i18nTexts.editPolicy.descriptions[phase]}
@ -115,20 +113,28 @@ export const Phase: FunctionComponent<Props> = ({ children, topLevelSettings, ph
<EuiSpacer size="m" />
)}
<EuiAccordion
id={`${phase}-settingsSwitch`}
buttonContent={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.phaseSettings.buttonLabel"
defaultMessage="Advanced settings"
/>
}
buttonClassName="ilmSettingsButton"
extraAction={<PhaseFooter phase={phase} />}
>
<EuiSpacer />
{children}
</EuiAccordion>
{children ? (
<EuiAccordion
id={`${phase}-settingsSwitch`}
buttonContent={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.phaseSettings.buttonLabel"
defaultMessage="Advanced settings"
/>
}
buttonClassName="ilmSettingsButton"
extraAction={<PhaseFooter phase={phase} />}
>
<EuiSpacer />
{children}
</EuiAccordion>
) : (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<PhaseFooter phase={phase} />
</EuiFlexItem>
</EuiFlexGroup>
)}
</>
)}
</EuiComment>

View file

@ -24,6 +24,17 @@ import './data_tier_allocation.scss';
type SelectOptions = EuiSuperSelectOption<DataTierAllocationType>;
const customTexts = {
inputDisplay: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.customOption.input',
{ defaultMessage: 'Custom' }
),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.customOption.helpText',
{ defaultMessage: 'Move data based on node attributes.' }
),
};
const i18nTexts = {
allocationOptions: {
warm: {
@ -47,16 +58,7 @@ const i18nTexts = {
{ 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.' }
),
},
custom: customTexts,
},
cold: {
default: {
@ -79,16 +81,30 @@ const i18nTexts = {
{ defaultMessage: 'Do not move data in the cold phase.' }
),
},
custom: {
inputDisplay: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input',
{ defaultMessage: 'Custom' }
custom: customTexts,
},
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.cold.customOption.helpText',
{ defaultMessage: 'Move data based on node attributes.' }
'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: customTexts,
},
},
};

View file

@ -21,6 +21,9 @@ const i18nTextsNodeRoleToDataTier: Record<DataTierRole, string> = {
data_cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel', {
defaultMessage: 'cold',
}),
data_frozen: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierFrozenLabel', {
defaultMessage: 'frozen',
}),
};
const i18nTexts = {
@ -49,6 +52,21 @@ const i18nTexts = {
values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] },
}),
},
frozen: {
title: i18n.translate(
'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.frozen.title',
{ defaultMessage: 'No nodes assigned to the frozen tier' }
),
body: (nodeRole: DataTierRole) =>
i18n.translate(
'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.frozen',
{
defaultMessage:
'This policy will move data in the frozen phase to {tier} tier nodes instead.',
values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] },
}
),
},
},
warning: {
warm: {

View file

@ -39,6 +39,19 @@ const i18nTexts = {
}
),
},
frozen: {
title: i18n.translate(
'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableTitle',
{ defaultMessage: 'No nodes assigned to the frozen tier' }
),
body: i18n.translate(
'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableBody',
{
defaultMessage:
'Assign at least one node to the frozen, cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.',
}
),
},
},
};

View file

@ -17,7 +17,7 @@ export { DefaultAllocationWarning } from './default_allocation_warning';
export { NoNodeAttributesWarning } from './no_node_attributes_warning';
export { MissingColdTierCallout } from './missing_cold_tier_callout';
export { MissingCloudTierCallout } from './missing_cloud_tier_callout';
export { CloudDataTierCallout } from './cloud_data_tier_callout';

View file

@ -9,20 +9,23 @@ import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import { EuiCallOut, EuiLink } from '@elastic/eui';
const i18nTexts = {
title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.title', {
defaultMessage: 'Create a cold tier',
const geti18nTexts = (tier: 'cold' | 'frozen') => ({
title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.title', {
defaultMessage: 'Create a {tier} tier',
values: { tier },
}),
body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.body', {
defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.',
body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.body', {
defaultMessage: 'Edit your Elastic Cloud deployment to set up a {tier} tier.',
values: { tier },
}),
linkText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.linkToCloudDeploymentDescription',
'xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.linkToCloudDeploymentDescription',
{ defaultMessage: 'View cloud deployment' }
),
};
});
interface Props {
phase: 'cold' | 'frozen';
linkToCloudDeployment?: string;
}
@ -31,9 +34,14 @@ interface Props {
* This may need to be change when we have autoscaling enabled on a cluster because nodes may not
* yet exist, but will automatically be provisioned.
*/
export const MissingColdTierCallout: FunctionComponent<Props> = ({ linkToCloudDeployment }) => {
export const MissingCloudTierCallout: FunctionComponent<Props> = ({
phase,
linkToCloudDeployment,
}) => {
const i18nTexts = geti18nTexts(phase);
return (
<EuiCallOut title={i18nTexts.title} data-test-subj="cloudMissingColdTierCallout">
<EuiCallOut title={i18nTexts.title} data-test-subj="cloudMissingTierCallout">
{i18nTexts.body}{' '}
{Boolean(linkToCloudDeployment) && (
<EuiLink href={linkToCloudDeployment} external>

View file

@ -33,6 +33,15 @@ const i18nTexts = {
}
),
},
frozen: {
body: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.frozen.nodeAttributesMissingDescription',
{
defaultMessage:
'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.',
}
),
},
};
export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAllocation }> = ({

View file

@ -25,7 +25,7 @@ import {
DefaultAllocationNotice,
DefaultAllocationWarning,
NoNodeAttributesWarning,
MissingColdTierCallout,
MissingCloudTierCallout,
CloudDataTierCallout,
LoadingError,
} from './components';
@ -71,17 +71,21 @@ export const DataTierAllocationField: FunctionComponent<Props> = ({ phase, descr
switch (allocationType) {
case 'node_roles':
/**
* We'll drive Cloud users to add a cold tier to their deployment if there are no nodes with the cold node role.
* We'll drive Cloud users to add a cold or frozen tier to their deployment if there are no nodes with that role.
*/
if (isCloudEnabled && phase === 'cold' && !isUsingDeprecatedDataRoleConfig) {
const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length;
if (
isCloudEnabled &&
!isUsingDeprecatedDataRoleConfig &&
(phase === 'cold' || phase === 'frozen')
) {
const hasNoNodesWithNodeRole = !nodesByRoles[`data_${phase}` as const]?.length;
if (hasDataNodeRoles && hasNoNodesWithNodeRole) {
// Tell cloud users they can deploy nodes on cloud.
return (
<>
<EuiSpacer size="s" />
<MissingColdTierCallout linkToCloudDeployment={cloudDeploymentUrl} />
<MissingCloudTierCallout phase={phase} linkToCloudDeployment={cloudDeploymentUrl} />
</>
);
}

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTextColor } from '@elastic/eui';
import { LearnMoreLink, ToggleFieldWithDescribedFormRow } from '../../';
interface Props {
phase: 'cold' | 'frozen';
}
export const FreezeField: FunctionComponent<Props> = ({ phase }) => {
return (
<ToggleFieldWithDescribedFormRow
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.freezeText"
defaultMessage="Freeze"
/>
</h3>
}
description={
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.freezeIndexExplanationText"
defaultMessage="Make the index read-only and minimize its memory footprint."
/>{' '}
<LearnMoreLink docPath="ilm-freeze.html" />
</EuiTextColor>
}
fullWidth
titleSize="xs"
switchProps={{
'data-test-subj': `${phase}-freezeSwitch`,
path: `_meta.${phase}.freezeEnabled`,
}}
>
<div />
</ToggleFieldWithDescribedFormRow>
);
};

View file

@ -22,3 +22,5 @@ export { ReadonlyField } from './readonly_field';
export { ReplicasField } from './replicas_field';
export { IndexPriorityField } from './index_priority_field';
export { FreezeField } from './freeze_field';

View file

@ -18,7 +18,7 @@ import { UseField } from '../../../form';
import { LearnMoreLink, DescribedFormRow } from '../..';
interface Props {
phase: 'hot' | 'warm' | 'cold';
phase: 'hot' | 'warm' | 'cold' | 'frozen';
}
export const IndexPriorityField: FunctionComponent<Props> = ({ phase }) => {

View file

@ -16,7 +16,7 @@ import { UseField } from '../../../form';
import { DescribedFormRow } from '../../described_form_row';
interface Props {
phase: 'warm' | 'cold';
phase: 'warm' | 'cold' | 'frozen';
}
export const ReplicasField: FunctionComponent<Props> = ({ phase }) => {

View file

@ -17,28 +17,18 @@ import {
EuiLink,
} from '@elastic/eui';
import {
ComboBoxField,
useKibana,
fieldValidators,
useFormData,
} from '../../../../../../../shared_imports';
import { ComboBoxField, useKibana, useFormData } from '../../../../../../../shared_imports';
import { useEditPolicyContext } from '../../../../edit_policy_context';
import { useConfigurationIssues, UseField } from '../../../../form';
import { i18nTexts } from '../../../../i18n_texts';
import { useConfigurationIssues, UseField, searchableSnapshotFields } from '../../../../form';
import { FieldLoadingError, DescribedFormRow, LearnMoreLink } from '../../../';
import { SearchableSnapshotDataProvider } from './searchable_snapshot_data_provider';
import './_searchable_snapshot_field.scss';
const { emptyField } = fieldValidators;
export interface Props {
phase: 'hot' | 'cold';
phase: 'hot' | 'cold' | 'frozen';
canBeDisabled?: boolean;
}
/**
@ -47,29 +37,62 @@ export interface Props {
*/
const CLOUD_DEFAULT_REPO = 'found-snapshots';
export const SearchableSnapshotField: FunctionComponent<Props> = ({ phase }) => {
const geti18nTexts = (phase: Props['phase']) => ({
title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', {
defaultMessage: 'Searchable snapshot',
}),
description:
phase === 'frozen' ? (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.frozenPhase.searchableSnapshotFieldDescription"
defaultMessage="Take a snapshot of your data and mount it as a searchable snapshot. To reduce costs, only a cache of the snapshot is mounted in the frozen tier. {learnMoreLink}"
values={{
learnMoreLink: (
<LearnMoreLink docPath="searchable-snapshots.html#searchable-snapshots-shared-cache" />
),
}}
/>
) : (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription"
defaultMessage="Take a snapshot of your data and mount it as a searchable snapshot. {learnMoreLink}"
values={{
learnMoreLink: <LearnMoreLink docPath="ilm-searchable-snapshot.html" />,
}}
/>
),
});
export const SearchableSnapshotField: FunctionComponent<Props> = ({
phase,
canBeDisabled = true,
}) => {
const {
services: { cloud },
} = useKibana();
const { getUrlForApp, policy, license, isNewPolicy } = useEditPolicyContext();
const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues();
const searchableSnapshotPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`;
const searchableSnapshotRepoPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`;
const [formData] = useFormData({ watch: searchableSnapshotPath });
const searchableSnapshotRepo = get(formData, searchableSnapshotPath);
const [formData] = useFormData({ watch: searchableSnapshotRepoPath });
const searchableSnapshotRepo = get(formData, searchableSnapshotRepoPath);
const isColdPhase = phase === 'cold';
const isFrozenPhase = phase === 'frozen';
const isColdOrFrozenPhase = isColdPhase || isFrozenPhase;
const isDisabledDueToLicense = !license.canUseSearchableSnapshot();
const [isFieldToggleChecked, setIsFieldToggleChecked] = useState(() =>
Boolean(
// New policy on cloud should have searchable snapshot on in cold phase
(isColdPhase && isNewPolicy && cloud?.isCloudEnabled) ||
// New policy on cloud should have searchable snapshot on in cold and frozen phase
(isColdOrFrozenPhase && isNewPolicy && cloud?.isCloudEnabled) ||
policy.phases[phase]?.actions?.searchable_snapshot?.snapshot_repository
)
);
const i18nTexts = geti18nTexts(phase);
useEffect(() => {
if (isDisabledDueToLicense) {
setIsFieldToggleChecked(false);
@ -180,17 +203,10 @@ export const SearchableSnapshotField: FunctionComponent<Props> = ({ phase }) =>
<div className="ilmSearchableSnapshotField">
<UseField<string>
config={{
label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel,
...searchableSnapshotFields.snapshot_repository,
defaultValue: cloud?.isCloudEnabled ? CLOUD_DEFAULT_REPO : undefined,
validations: [
{
validator: emptyField(
i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired
),
},
],
}}
path={searchableSnapshotPath}
path={searchableSnapshotRepoPath}
>
{(field) => {
const singleSelectionArray: [selectedSnapshot?: string] = field.value
@ -289,34 +305,24 @@ export const SearchableSnapshotField: FunctionComponent<Props> = ({ phase }) =>
return (
<DescribedFormRow
data-test-subj={`searchableSnapshotField-${phase}`}
switchProps={{
checked: isFieldToggleChecked,
disabled: isDisabledDueToLicense,
onChange: setIsFieldToggleChecked,
'data-test-subj': 'searchableSnapshotToggle',
label: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotsToggleLabel',
{ defaultMessage: 'Create searchable snapshot' }
),
}}
title={
<h3>
{i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', {
defaultMessage: 'Searchable snapshot',
})}
</h3>
switchProps={
canBeDisabled
? {
checked: isFieldToggleChecked,
disabled: isDisabledDueToLicense,
onChange: setIsFieldToggleChecked,
'data-test-subj': 'searchableSnapshotToggle',
label: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotsToggleLabel',
{ defaultMessage: 'Create searchable snapshot' }
),
}
: undefined
}
title={<h3>{i18nTexts.title}</h3>}
description={
<>
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription"
defaultMessage="Take a snapshot of the managed index in the selected repository and mount it as a searchable snapshot. {learnMoreLink}"
values={{
learnMoreLink: <LearnMoreLink docPath="ilm-searchable-snapshot.html" />,
}}
/>
</EuiTextColor>
<EuiTextColor color="subdued">{i18nTexts.description}</EuiTextColor>
</>
}
fieldNotices={renderInfoCallout()}

View file

@ -42,6 +42,7 @@ const prettifyFormJson = (policy: SerializedPolicy): SerializedPolicy => ({
hot: policy.phases.hot,
warm: policy.phases.warm,
cold: policy.phases.cold,
frozen: policy.phases.frozen,
delete: policy.phases.delete,
},
});

View file

@ -26,6 +26,7 @@ export const Timeline: FunctionComponent = () => {
hotPhaseMinAge={timings.hot.min_age}
warmPhaseMinAge={timings.warm?.min_age}
coldPhaseMinAge={timings.cold?.min_age}
frozenPhaseMinAge={timings.frozen?.min_age}
deletePhaseMinAge={timings.delete?.min_age}
isUsingRollover={isUsingRollover}
hasDeletePhase={Boolean(formData._meta?.delete?.enabled)}

View file

@ -80,4 +80,12 @@ $ilmTimelineBarHeight: $euiSizeS;
background-color: $euiColorVis1;
}
}
&__frozenPhase {
width: var(--ilm-timeline-frozen-phase-width);
&__colorBar {
background-color: $euiColorVis4;
}
}
}

View file

@ -62,6 +62,9 @@ const i18nTexts = {
coldPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.coldPhaseSectionTitle', {
defaultMessage: 'Cold phase',
}),
frozenPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.frozenPhaseSectionTitle', {
defaultMessage: 'Frozen phase',
}),
deleteIcon: {
toolTipContent: i18n.translate('xpack.indexLifecycleMgmt.timeline.deleteIconToolTipContent', {
defaultMessage: 'Policy deletes the index after lifecycle phases complete.',
@ -84,12 +87,17 @@ const calculateWidths = (inputs: PhaseAgeInMilliseconds) => {
inputs.phases.cold != null
? msTimeToOverallPercent(inputs.phases.cold, inputs.total) + SCORE_BUFFER_AMOUNT
: 0;
const frozenScore =
inputs.phases.frozen != null
? msTimeToOverallPercent(inputs.phases.frozen, inputs.total) + SCORE_BUFFER_AMOUNT
: 0;
const totalScore = hotScore + warmScore + coldScore;
const totalScore = hotScore + warmScore + coldScore + frozenScore;
return {
hot: `${toPercent(hotScore, totalScore)}%`,
warm: `${toPercent(warmScore, totalScore)}%`,
cold: `${toPercent(coldScore, totalScore)}%`,
frozen: `${toPercent(frozenScore, totalScore)}%`,
};
};
@ -102,6 +110,7 @@ interface Props {
isUsingRollover: boolean;
warmPhaseMinAge?: string;
coldPhaseMinAge?: string;
frozenPhaseMinAge?: string;
deletePhaseMinAge?: string;
}
@ -115,6 +124,9 @@ export const Timeline: FunctionComponent<Props> = memo(
hot: { min_age: phasesMinAge.hotPhaseMinAge },
warm: phasesMinAge.warmPhaseMinAge ? { min_age: phasesMinAge.warmPhaseMinAge } : undefined,
cold: phasesMinAge.coldPhaseMinAge ? { min_age: phasesMinAge.coldPhaseMinAge } : undefined,
frozen: phasesMinAge.frozenPhaseMinAge
? { min_age: phasesMinAge.frozenPhaseMinAge }
: undefined,
delete: phasesMinAge.deletePhaseMinAge
? { min_age: phasesMinAge.deletePhaseMinAge }
: undefined,
@ -157,6 +169,7 @@ export const Timeline: FunctionComponent<Props> = memo(
el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot);
el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null);
el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null);
el.style.setProperty('--ilm-timeline-frozen-phase-width', widths.frozen ?? null);
}
}}
>
@ -198,6 +211,18 @@ export const Timeline: FunctionComponent<Props> = memo(
/>
</div>
)}
{exists(phaseAgeInMilliseconds.phases.frozen) && (
<div
data-test-subj="ilmTimelineFrozenPhase"
className="ilmTimeline__phasesContainer__phase ilmTimeline__frozenPhase"
>
<div className="ilmTimeline__colorBar ilmTimeline__frozenPhase__colorBar" />
<TimelinePhaseText
phaseName={i18nTexts.frozenPhase}
durationInPhase={getDurationInPhaseContent('frozen')}
/>
</div>
)}
</div>
</EuiFlexItem>
{hasDeletePhase && (

View file

@ -43,6 +43,7 @@ import {
ColdPhase,
DeletePhase,
HotPhase,
FrozenPhase,
PolicyJsonFlyout,
WarmPhase,
Timeline,
@ -72,6 +73,7 @@ export const EditPolicy: React.FunctionComponent<Props> = ({ history }) => {
policy: currentPolicy,
existingPolicies,
policyName,
license,
} = useEditPolicyContext();
const serializer = useMemo(() => {
@ -80,6 +82,7 @@ export const EditPolicy: React.FunctionComponent<Props> = ({ history }) => {
const [saveAsNew, setSaveAsNew] = useState(false);
const originalPolicyName: string = isNewPolicy ? '' : policyName!;
const isAllowedByLicense = license.canUseSearchableSnapshot();
const { form } = useForm({
schema,
@ -243,13 +246,21 @@ export const EditPolicy: React.FunctionComponent<Props> = ({ history }) => {
<HotPhase />
<EuiSpacer />
<WarmPhase />
<EuiSpacer />
<ColdPhase />
{isAllowedByLicense && (
<>
<EuiSpacer />
<FrozenPhase />
</>
)}
{/* We can't add the <EuiSpacer /> here as it breaks the layout
and makes the connecting line go further that it needs to.
There is an issue in EUI to fix this (https://github.com/elastic/eui/issues/4492) */}
<DeletePhase />
</div>

View file

@ -23,6 +23,7 @@ const isXPhaseField = (phase: keyof Phases) => (fieldPath: string): boolean =>
const isHotPhaseField = isXPhaseField('hot');
const isWarmPhaseField = isXPhaseField('warm');
const isColdPhaseField = isXPhaseField('cold');
const isFrozenPhaseField = isXPhaseField('frozen');
const isDeletePhaseField = isXPhaseField('delete');
const determineFieldPhase = (fieldPath: string): keyof Phases | 'other' => {
@ -35,6 +36,9 @@ const determineFieldPhase = (fieldPath: string): keyof Phases | 'other' => {
if (isColdPhaseField(fieldPath)) {
return 'cold';
}
if (isFrozenPhaseField(fieldPath)) {
return 'frozen';
}
if (isDeletePhaseField(fieldPath)) {
return 'delete';
}

View file

@ -25,6 +25,7 @@ export interface ConfigurationIssues {
* See https://github.com/elastic/elasticsearch/blob/master/docs/reference/ilm/actions/ilm-searchable-snapshot.asciidoc.
*/
isUsingSearchableSnapshotInHotPhase: boolean;
isUsingSearchableSnapshotInColdPhase: boolean;
}
const ConfigurationIssuesContext = createContext<ConfigurationIssues>(null as any);
@ -32,10 +33,14 @@ const ConfigurationIssuesContext = createContext<ConfigurationIssues>(null as an
const pathToHotPhaseSearchableSnapshot =
'phases.hot.actions.searchable_snapshot.snapshot_repository';
const pathToColdPhaseSearchableSnapshot =
'phases.cold.actions.searchable_snapshot.snapshot_repository';
export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => {
const [formData] = useFormData({
watch: [
pathToHotPhaseSearchableSnapshot,
pathToColdPhaseSearchableSnapshot,
isUsingCustomRolloverPath,
isUsingDefaultRolloverPath,
],
@ -50,6 +55,8 @@ export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) =>
isUsingRollover: isUsingDefaultRollover === false ? isUsingCustomRollover : true,
isUsingSearchableSnapshotInHotPhase:
get(formData, pathToHotPhaseSearchableSnapshot) != null,
isUsingSearchableSnapshotInColdPhase:
get(formData, pathToColdPhaseSearchableSnapshot) != null,
}}
>
{children}

View file

@ -17,7 +17,7 @@ import { FormInternal } from '../types';
export const deserializer = (policy: SerializedPolicy): FormInternal => {
const {
phases: { hot, warm, cold, delete: deletePhase },
phases: { hot, warm, cold, frozen, delete: deletePhase },
} = policy;
const _meta: FormInternal['_meta'] = {
@ -41,6 +41,11 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => {
dataTierAllocationType: determineDataTierAllocationType(cold?.actions),
freezeEnabled: Boolean(cold?.actions?.freeze),
},
frozen: {
enabled: Boolean(frozen),
dataTierAllocationType: determineDataTierAllocationType(frozen?.actions),
freezeEnabled: Boolean(frozen?.actions?.freeze),
},
delete: {
enabled: Boolean(deletePhase),
},

View file

@ -26,6 +26,7 @@ interface Errors {
hot: ErrorGroup;
warm: ErrorGroup;
cold: ErrorGroup;
frozen: ErrorGroup;
delete: ErrorGroup;
/**
* Errors that are not specific to a phase should go here.
@ -46,6 +47,7 @@ const createEmptyErrors = (): Errors => ({
hot: {},
warm: {},
cold: {},
frozen: {},
delete: {},
other: {},
});

View file

@ -9,7 +9,7 @@ export { deserializer } from './deserializer';
export { createSerializer } from './serializer';
export { schema } from './schema';
export { schema, searchableSnapshotFields } from './schema';
export * from './validations';

View file

@ -23,19 +23,24 @@ const getPhaseTimingConfiguration = (
hot: PhaseTimingConfiguration;
warm: PhaseTimingConfiguration;
cold: PhaseTimingConfiguration;
frozen: PhaseTimingConfiguration;
} => {
const isWarmPhaseEnabled = formData?._meta?.warm?.enabled;
const isColdPhaseEnabled = formData?._meta?.cold?.enabled;
const isFrozenPhaseEnabled = formData?._meta?.frozen?.enabled;
return {
hot: { isFinalDataPhase: !isWarmPhaseEnabled && !isColdPhaseEnabled },
warm: { isFinalDataPhase: isWarmPhaseEnabled && !isColdPhaseEnabled },
cold: { isFinalDataPhase: isColdPhaseEnabled },
hot: { isFinalDataPhase: !isWarmPhaseEnabled && !isColdPhaseEnabled && !isFrozenPhaseEnabled },
warm: { isFinalDataPhase: isWarmPhaseEnabled && !isColdPhaseEnabled && !isFrozenPhaseEnabled },
cold: { isFinalDataPhase: isColdPhaseEnabled && !isFrozenPhaseEnabled },
frozen: { isFinalDataPhase: isFrozenPhaseEnabled },
};
};
export interface PhaseTimings {
hot: PhaseTimingConfiguration;
warm: PhaseTimingConfiguration;
cold: PhaseTimingConfiguration;
frozen: PhaseTimingConfiguration;
isDeletePhaseEnabled: boolean;
setDeletePhaseEnabled: (enabled: boolean) => void;
}
@ -44,7 +49,12 @@ const PhaseTimingsContext = createContext<PhaseTimings>(null as any);
export const PhaseTimingsProvider: FunctionComponent = ({ children }) => {
const [formData] = useFormData<FormInternal>({
watch: ['_meta.warm.enabled', '_meta.cold.enabled', '_meta.delete.enabled'],
watch: [
'_meta.warm.enabled',
'_meta.cold.enabled',
'_meta.frozen.enabled',
'_meta.delete.enabled',
],
});
return (
@ -65,6 +75,7 @@ export const PhaseTimingsProvider: FunctionComponent = ({ children }) => {
</UseField>
);
};
export const usePhaseTimings = () => {
const ctx = useContext(PhaseTimingsContext);
if (!ctx) throw new Error('Cannot use phase timings outside of phase timings context');

View file

@ -30,6 +30,90 @@ const serializers = {
stringToNumber: (v: string): any => (v != null ? parseInt(v, 10) : undefined),
};
const maxNumSegmentsField = {
label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel,
validations: [
{
validator: emptyField(
i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError',
{ defaultMessage: 'A value for number of segments is required.' }
)
),
},
{
validator: ifExistsNumberGreaterThanZero,
},
],
serializer: serializers.stringToNumber,
};
export const searchableSnapshotFields = {
snapshot_repository: {
label: i18nTexts.editPolicy.searchableSnapshotsRepoFieldLabel,
validations: [
{ validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) },
],
},
storage: {
label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageLabel', {
defaultMessage: 'Storage',
}),
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageHelpText',
{
defaultMessage:
"Type of snapshot mounted for the searchable snapshot. This is an advanced option. Only change it if you know what you're doing.",
}
),
},
};
const numberOfReplicasField = {
label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.numberOfReplicasLabel', {
defaultMessage: 'Number of replicas',
}),
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{
validator: ifExistsNumberNonNegative,
},
],
serializer: serializers.stringToNumber,
};
const numberOfShardsField = {
label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', {
defaultMessage: 'Number of primary shards',
}),
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{
validator: numberGreaterThanField({
message: i18nTexts.editPolicy.errors.numberGreatThan0Required,
than: 0,
}),
},
],
serializer: serializers.stringToNumber,
};
const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({
defaultValue: defaultIndexPriority[phase] as any,
label: i18nTexts.editPolicy.indexPriorityFieldLabel,
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{ validator: ifExistsNumberNonNegative },
],
serializer: serializers.stringToNumber,
});
export const schema: FormSchema<FormInternal> = {
_meta: {
hot: {
@ -110,6 +194,30 @@ export const schema: FormSchema<FormInternal> = {
label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel,
},
},
frozen: {
enabled: {
defaultValue: false,
label: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.activateFrozenPhaseSwitchLabel',
{ defaultMessage: 'Activate frozen phase' }
),
},
freezeEnabled: {
defaultValue: false,
label: i18n.translate('xpack.indexLifecycleMgmt.frozePhase.freezeIndexLabel', {
defaultMessage: 'Freeze index',
}),
},
minAgeUnit: {
defaultValue: 'd',
},
dataTierAllocationType: {
label: i18nTexts.editPolicy.allocationTypeOptionsFieldLabel,
},
allocationNodeAttribute: {
label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel,
},
},
delete: {
enabled: {
defaultValue: false,
@ -172,55 +280,13 @@ export const schema: FormSchema<FormInternal> = {
},
},
forcemerge: {
max_num_segments: {
label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel,
validations: [
{
validator: emptyField(
i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError',
{ defaultMessage: 'A value for number of segments is required.' }
)
),
},
{
validator: ifExistsNumberGreaterThanZero,
},
],
serializer: serializers.stringToNumber,
},
max_num_segments: maxNumSegmentsField,
},
shrink: {
number_of_shards: {
label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', {
defaultMessage: 'Number of primary shards',
}),
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{
validator: numberGreaterThanField({
message: i18nTexts.editPolicy.errors.numberGreatThan0Required,
than: 0,
}),
},
],
serializer: serializers.stringToNumber,
},
number_of_shards: numberOfShardsField,
},
set_priority: {
priority: {
defaultValue: defaultIndexPriority.hot as any,
label: i18nTexts.editPolicy.indexPriorityFieldLabel,
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{ validator: ifExistsNumberNonNegative },
],
serializer: serializers.stringToNumber,
},
priority: getPriorityField('hot'),
},
},
},
@ -235,71 +301,16 @@ export const schema: FormSchema<FormInternal> = {
},
actions: {
allocate: {
number_of_replicas: {
label: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel', {
defaultMessage: 'Number of replicas',
}),
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{
validator: ifExistsNumberNonNegative,
},
],
serializer: serializers.stringToNumber,
},
number_of_replicas: numberOfReplicasField,
},
shrink: {
number_of_shards: {
label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', {
defaultMessage: 'Number of primary shards',
}),
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{
validator: numberGreaterThanField({
message: i18nTexts.editPolicy.errors.numberGreatThan0Required,
than: 0,
}),
},
],
serializer: serializers.stringToNumber,
},
number_of_shards: numberOfShardsField,
},
forcemerge: {
max_num_segments: {
label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel,
validations: [
{
validator: emptyField(
i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError',
{ defaultMessage: 'A value for number of segments is required.' }
)
),
},
{
validator: ifExistsNumberGreaterThanZero,
},
],
serializer: serializers.stringToNumber,
},
max_num_segments: maxNumSegmentsField,
},
set_priority: {
priority: {
defaultValue: defaultIndexPriority.warm as any,
label: i18nTexts.editPolicy.indexPriorityFieldLabel,
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{ validator: ifExistsNumberNonNegative },
],
serializer: serializers.stringToNumber,
},
priority: getPriorityField('warm'),
},
},
},
@ -314,42 +325,31 @@ export const schema: FormSchema<FormInternal> = {
},
actions: {
allocate: {
number_of_replicas: {
label: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel', {
defaultMessage: 'Number of replicas',
}),
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{
validator: ifExistsNumberNonNegative,
},
],
serializer: serializers.stringToNumber,
},
number_of_replicas: numberOfReplicasField,
},
set_priority: {
priority: {
defaultValue: defaultIndexPriority.cold as any,
label: i18nTexts.editPolicy.indexPriorityFieldLabel,
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
},
{ validator: ifExistsNumberNonNegative },
],
serializer: serializers.stringToNumber,
},
priority: getPriorityField('cold'),
},
searchable_snapshot: {
snapshot_repository: {
label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel,
validations: [
{ validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) },
],
searchable_snapshot: searchableSnapshotFields,
},
},
frozen: {
min_age: {
defaultValue: '0',
validations: [
{
validator: minAgeValidator,
},
],
},
actions: {
allocate: {
number_of_replicas: numberOfReplicasField,
},
set_priority: {
priority: getPriorityField('frozen'),
},
searchable_snapshot: searchableSnapshotFields,
},
},
delete: {

View file

@ -241,6 +241,23 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => (
delete draft.phases.cold;
}
/**
* FROZEN PHASE SERIALIZATION
*/
if (_meta.frozen.enabled) {
draft.phases.frozen!.actions = draft.phases.frozen?.actions ?? {};
const frozenPhase = draft.phases.frozen!;
/**
* FROZEN PHASE SEARCHABLE SNAPSHOT
*/
if (!updatedPolicy.phases.frozen?.actions?.searchable_snapshot) {
delete frozenPhase.actions.searchable_snapshot;
}
} else {
delete draft.phases.frozen;
}
/**
* DELETE PHASE SERIALIZATION
*/

View file

@ -77,12 +77,18 @@ export const i18nTexts = {
defaultMessage: 'Select a node attribute',
}
),
searchableSnapshotsFieldLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel',
searchableSnapshotsRepoFieldLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel',
{
defaultMessage: 'Searchable snapshot repository',
}
),
searchableSnapshotsStorageFieldLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotStorageFieldLabel',
{
defaultMessage: 'Searchable snapshot storage',
}
),
errors: {
numberRequired: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.errors.numberRequiredErrorMessage',
@ -188,6 +194,9 @@ export const i18nTexts = {
cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle', {
defaultMessage: 'Cold phase',
}),
frozen: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.frozenPhase.frozenPhaseTitle', {
defaultMessage: 'Frozen phase',
}),
delete: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseTitle', {
defaultMessage: 'Delete phase',
}),
@ -205,6 +214,13 @@ export const i18nTexts = {
defaultMessage:
'Move data to the cold tier, which is optimized for cost savings over search performance. Data is normally read-only in the cold phase.',
}),
frozen: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.frozenPhaseDescription',
{
defaultMessage:
'Archive data as searchable snapshots in the frozen tier. The frozen tier is optimized for maximum cost savings. Data in the frozen tier is rarely accessed and never updated.',
}
),
delete: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescription',
{

View file

@ -28,11 +28,11 @@ import { FormInternal } from '../types';
/* -===- Private functions and types -===- */
type MinAgePhase = 'warm' | 'cold' | 'delete';
type MinAgePhase = 'warm' | 'cold' | 'frozen' | 'delete';
type Phase = 'hot' | MinAgePhase;
const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete'];
const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'frozen', 'delete'];
const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({
min_age: formData.phases?.[phase]?.min_age
@ -69,6 +69,9 @@ export interface AbsoluteTimings {
cold?: {
min_age: string;
};
frozen?: {
min_age: string;
};
delete?: {
min_age: string;
};
@ -80,6 +83,7 @@ export interface PhaseAgeInMilliseconds {
hot: number;
warm?: number;
cold?: number;
frozen?: number;
};
}
@ -92,6 +96,7 @@ export const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimin
hot: { min_age: undefined },
warm: _meta.warm.enabled ? getMinAge('warm', formData) : undefined,
cold: _meta.cold.enabled ? getMinAge('cold', formData) : undefined,
frozen: _meta.frozen?.enabled ? getMinAge('frozen', formData) : undefined,
delete: _meta.delete.enabled ? getMinAge('delete', formData) : undefined,
};
};
@ -139,6 +144,7 @@ export const calculateRelativeFromAbsoluteMilliseconds = (
hot: 0,
warm: inputs.warm ? 0 : undefined,
cold: inputs.cold ? 0 : undefined,
frozen: inputs.frozen ? 0 : undefined,
},
}
);

View file

@ -53,6 +53,11 @@ interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField {
freezeEnabled: boolean;
}
interface FrozenPhaseMetaFields extends DataAllocationMetaFields, MinAgeField {
enabled: boolean;
freezeEnabled: boolean;
}
interface DeletePhaseMetaFields extends MinAgeField {
enabled: boolean;
}
@ -69,6 +74,7 @@ export interface FormInternal extends SerializedPolicy {
hot: HotPhaseMetaFields;
warm: WarmPhaseMetaFields;
cold: ColdPhaseMetaFields;
frozen: FrozenPhaseMetaFields;
delete: DeletePhaseMetaFields;
};
}

View file

@ -75,6 +75,7 @@ export function registerListRoute({
'ml.enabled',
'ml.machine_memory',
'ml.max_open_jobs',
'ml.max_jvm_size',
// Used by ML to identify nodes that have transform enabled:
// https://github.com/elastic/elasticsearch/pull/52712/files#diff-225cc2c1291b4c60a8c3412a619094e1R147
'transform.node',

View file

@ -37,6 +37,7 @@ const bodySchema = schema.object({
hot: schema.any(),
warm: schema.maybe(schema.any()),
cold: schema.maybe(schema.any()),
frozen: schema.maybe(schema.any()),
delete: schema.maybe(schema.any()),
}),
});

View file

@ -10046,7 +10046,6 @@
"xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "コールドティアに割り当てられているノードがありません",
"xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。安価なハードウェアのコールドフェーズにデータを格納します。",
"xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結",
"xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "レプリカの数",
"xpack.indexLifecycleMgmt.common.dataTier.title": "データ割り当て",
"xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "キャンセル",
"xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "削除",
@ -10061,16 +10060,10 @@
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "コールドフェーズを有効にする",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription": "データをコールドティアに移動します。これは、検索パフォーマンスよりもコスト削減を優先するように最適化されています。通常、コールドフェーズではデータが読み取り専用です。",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle": "コールドフェーズ",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "インデックスを読み取り専用にし、メモリー消費量を最小化します。",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "凍結",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "ノード属性に基づいてデータを移動します。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "カスタム",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "コールドティアのノードにデータを移動します。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input": "コールドノードを使用 (推奨) ",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText": "コールドフェーズにデータを移動しないでください。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input": "オフ",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText": "ノード属性に基づいてデータを移動します。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input": "カスタム",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText": "ウォームティアのノードにデータを移動します。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input": "ウォームノードを使用 (推奨) ",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText": "ウォームフェーズにデータを移動しないでください。",
@ -10183,7 +10176,6 @@
"xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "ライフサイクルポリシー {lifecycleName} の保存中にエラーが発生しました",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "検索可能なスナップショットがホットフェーズで有効な場合には、強制、マージ、縮小、凍結、コールドフェーズの検索可能なスナップショットは許可されません。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "選択したリポジトリで管理されたインデックスのスナップショットを作成し、検索可能なスナップショットとしてマウントします。{learnMoreLink}",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "検索可能なスナップショットリポジドリ",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "検索可能スナップショット",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "検索可能なスナップショットを作成するには、エンタープライズライセンスが必要です。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "エンタープライズライセンスが必要です",
@ -10349,7 +10341,6 @@
"xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "このポリシーはウォームフェーズのデータを{tier}ティアノードに移動します。",
"xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "ウォームティアに割り当てられているノードがありません",
"xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。",
"xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "レプリカの数",
"xpack.infra.alerting.alertDropdownTitle": "アラート",
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし (グループなし) ",
"xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件",

View file

@ -10173,7 +10173,6 @@
"xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "没有分配到冷层的节点",
"xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。将处于冷阶段的数据存储在成本较低的硬件上。",
"xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引",
"xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "副本分片数目",
"xpack.indexLifecycleMgmt.common.dataTier.title": "数据分配",
"xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "取消",
"xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "删除",
@ -10188,16 +10187,10 @@
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "激活冷阶段",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription": "将数据移到经过优化后节省了成本但牺牲了搜索性能的冷层。数据在冷阶段通常为只读。",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle": "冷阶段",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "使索引只读,并最大限度减小其内存占用。",
"xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "冻结",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "根据节点属性移动数据。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "定制",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "将数据移到冷层中的节点。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input": "使用冷节点 (建议) ",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText": "不要移动冷阶段的数据。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input": "关闭",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText": "根据节点属性移动数据。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input": "定制",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText": "将数据移到温层中的节点。",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input": "使用温节点 (建议) ",
"xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText": "不要移动温阶段的数据。",
@ -10310,7 +10303,6 @@
"xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "保存生命周期策略 {lifecycleName} 时出错",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "在热阶段启用可搜索快照时,不允许强制合并、缩小、冻结可搜索快照以及将其置入冷阶段。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "在所选存储库中拍取受管索引的快照,并将其安装为可搜索快照。{learnMoreLink}",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "可搜索快照存储库",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "可搜索快照",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "要创建可搜索快照,需要企业许可证。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "需要企业许可证",
@ -10480,7 +10472,6 @@
"xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "此策略会改为将温阶段的数据移到{tier}层节点。",
"xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "没有分配到温层的节点",
"xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。",
"xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "副本分片数目",
"xpack.infra.alerting.alertDropdownTitle": "告警",
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容 (未分组) ",
"xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据",

View file

@ -75,8 +75,16 @@ export const getPolicyPayload = (name) => ({
},
},
},
frozen: {
min_age: '20d',
actions: {
searchable_snapshot: {
snapshot_repository: 'backing_repo',
},
},
},
delete: {
min_age: '10d',
min_age: '30d',
actions: {
wait_for_snapshot: {
policy: 'policy',