mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Index Management] Add data retention support to index template (#170609)
This commit is contained in:
parent
65e65a834e
commit
f7ab883319
22 changed files with 476 additions and 19 deletions
|
@ -12,7 +12,7 @@ import { API_BASE_PATH } from '../../../common/constants';
|
|||
import { getComposableTemplate } from '../../../test/fixtures';
|
||||
import { setupEnvironment } from '../helpers';
|
||||
|
||||
import { TEMPLATE_NAME, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS, MAPPINGS } from './constants';
|
||||
import { TEMPLATE_NAME, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS } from './constants';
|
||||
import { setup } from './template_clone.helpers';
|
||||
import { TemplateFormTestBed } from './template_form.helpers';
|
||||
|
||||
|
@ -37,9 +37,7 @@ jest.mock('@elastic/eui', () => {
|
|||
const templateToClone = getComposableTemplate({
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: ['indexPattern1'],
|
||||
template: {
|
||||
mappings: MAPPINGS,
|
||||
},
|
||||
template: {},
|
||||
allowAutoCreate: true,
|
||||
});
|
||||
|
||||
|
@ -98,7 +96,7 @@ describe('<TemplateClone />', () => {
|
|||
actions.clickNextButton();
|
||||
});
|
||||
|
||||
const { priority, version, _kbnMeta, allowAutoCreate } = templateToClone;
|
||||
const { template, priority, version, _kbnMeta, allowAutoCreate } = templateToClone;
|
||||
expect(httpSetup.post).toHaveBeenLastCalledWith(
|
||||
`${API_BASE_PATH}/index_templates`,
|
||||
expect.objectContaining({
|
||||
|
@ -109,6 +107,7 @@ describe('<TemplateClone />', () => {
|
|||
version,
|
||||
allowAutoCreate,
|
||||
_kbnMeta,
|
||||
template,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
|
|
@ -532,6 +532,12 @@ describe('<TemplateCreate />', () => {
|
|||
await actions.completeStepOne({
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
dataStream: {},
|
||||
lifecycle: {
|
||||
enabled: true,
|
||||
value: 1,
|
||||
unit: 'd',
|
||||
},
|
||||
allowAutoCreate: true,
|
||||
});
|
||||
// Component templates
|
||||
|
@ -560,6 +566,7 @@ describe('<TemplateCreate />', () => {
|
|||
name: TEMPLATE_NAME,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
allowAutoCreate: true,
|
||||
dataStream: {},
|
||||
_kbnMeta: {
|
||||
type: 'default',
|
||||
hasDatastream: false,
|
||||
|
@ -582,6 +589,10 @@ describe('<TemplateCreate />', () => {
|
|||
},
|
||||
},
|
||||
aliases: ALIASES,
|
||||
lifecycle: {
|
||||
enabled: true,
|
||||
data_retention: '1d',
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
@ -621,6 +632,11 @@ describe('<TemplateCreate />', () => {
|
|||
name: TEMPLATE_NAME,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
dataStream: {},
|
||||
lifecycle: {
|
||||
enabled: true,
|
||||
value: 1,
|
||||
unit: 'd',
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
|
@ -631,6 +647,12 @@ describe('<TemplateCreate />', () => {
|
|||
`${API_BASE_PATH}/index_templates/simulate`,
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
template: {
|
||||
lifecycle: {
|
||||
enabled: true,
|
||||
data_retention: '1d',
|
||||
},
|
||||
},
|
||||
index_patterns: DEFAULT_INDEX_PATTERNS,
|
||||
data_stream: {},
|
||||
allow_auto_create: false,
|
||||
|
|
|
@ -119,6 +119,11 @@ describe('<TemplateEdit />', () => {
|
|||
name: 'test',
|
||||
indexPatterns: ['myPattern*'],
|
||||
version: 1,
|
||||
lifecycle: {
|
||||
enabled: true,
|
||||
value: 1,
|
||||
unit: 'd',
|
||||
},
|
||||
});
|
||||
// Component templates
|
||||
await actions.completeStepTwo();
|
||||
|
@ -150,6 +155,12 @@ describe('<TemplateEdit />', () => {
|
|||
hasDatastream: true,
|
||||
isLegacy: false,
|
||||
},
|
||||
template: {
|
||||
lifecycle: {
|
||||
enabled: true,
|
||||
data_retention: '1d',
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
|
|
@ -146,6 +146,7 @@ export const formSetup = async (initTestBed: SetupFunc<TestSubjects>) => {
|
|||
priority,
|
||||
version,
|
||||
dataStream,
|
||||
lifecycle,
|
||||
allowAutoCreate,
|
||||
}: Partial<TemplateDeserialized> = {}) => {
|
||||
const { component, form, find } = testBed;
|
||||
|
@ -184,12 +185,27 @@ export const formSetup = async (initTestBed: SetupFunc<TestSubjects>) => {
|
|||
if (version) {
|
||||
form.setInputValue('versionField.input', JSON.stringify(version));
|
||||
}
|
||||
});
|
||||
component.update();
|
||||
|
||||
if (lifecycle && lifecycle.enabled) {
|
||||
act(() => {
|
||||
form.toggleEuiSwitch('dataRetentionToggle.input');
|
||||
});
|
||||
component.update();
|
||||
|
||||
act(() => {
|
||||
form.setInputValue('valueDataRetentionField', String(lifecycle.value));
|
||||
});
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
if (allowAutoCreate) {
|
||||
form.toggleEuiSwitch('allowAutoCreateField.input');
|
||||
}
|
||||
|
||||
clickNextButton();
|
||||
jest.advanceTimersByTime(0);
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
@ -215,7 +231,6 @@ export const formSetup = async (initTestBed: SetupFunc<TestSubjects>) => {
|
|||
|
||||
await act(async () => {
|
||||
clickNextButton();
|
||||
jest.advanceTimersByTime(0);
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
@ -337,6 +352,7 @@ export type TestSubjects =
|
|||
| 'orderField.input'
|
||||
| 'priorityField.input'
|
||||
| 'dataStreamField.input'
|
||||
| 'dataRetentionToggle.input'
|
||||
| 'allowAutoCreateField.input'
|
||||
| 'pageTitle'
|
||||
| 'previewTab'
|
||||
|
@ -361,6 +377,7 @@ export type TestSubjects =
|
|||
| 'aliasesEditor'
|
||||
| 'settingsEditor'
|
||||
| 'versionField.input'
|
||||
| 'valueDataRetentionField'
|
||||
| 'mappingsEditor.formTab'
|
||||
| 'mappingsEditor.advancedConfiguration.sizeEnabledToggle'
|
||||
| 'previewIndexTemplate';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataStream, EnhancedDataStreamFromEs, Health } from '../types';
|
||||
import { DataStream, EnhancedDataStreamFromEs, Health, DataRetention } from '../types';
|
||||
|
||||
export function deserializeDataStream(dataStreamFromEs: EnhancedDataStreamFromEs): DataStream {
|
||||
const {
|
||||
|
@ -83,3 +83,43 @@ export const splitSizeAndUnits = (field: string): { size: string; unit: string }
|
|||
unit,
|
||||
};
|
||||
};
|
||||
|
||||
export const serializeAsESLifecycle = (lifecycle?: DataRetention): DataStream['lifecycle'] => {
|
||||
if (!lifecycle || !lifecycle?.enabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { infiniteDataRetention, value, unit } = lifecycle;
|
||||
|
||||
if (infiniteDataRetention) {
|
||||
return {
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
data_retention: `${value}${unit}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const deserializeESLifecycle = (lifecycle?: DataStream['lifecycle']): DataRetention => {
|
||||
if (!lifecycle || !lifecycle?.enabled) {
|
||||
return { enabled: false };
|
||||
}
|
||||
|
||||
if (!lifecycle.data_retention) {
|
||||
return {
|
||||
enabled: true,
|
||||
infiniteDataRetention: true,
|
||||
};
|
||||
}
|
||||
|
||||
const { size, unit } = splitSizeAndUnits(lifecycle.data_retention as string);
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
value: Number(size),
|
||||
unit,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
TemplateListItem,
|
||||
TemplateType,
|
||||
} from '../types';
|
||||
import { deserializeESLifecycle } from './data_stream_serialization';
|
||||
|
||||
const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
|
||||
|
||||
|
@ -69,6 +70,7 @@ export function deserializeTemplate(
|
|||
name,
|
||||
version,
|
||||
priority,
|
||||
...(template.lifecycle ? { lifecycle: deserializeESLifecycle(template.lifecycle) } : {}),
|
||||
indexPatterns: indexPatterns.sort(),
|
||||
template,
|
||||
ilmPolicy: settings?.index?.lifecycle,
|
||||
|
|
|
@ -75,3 +75,10 @@ export interface DataStreamIndex {
|
|||
preferILM: boolean;
|
||||
managedBy: string;
|
||||
}
|
||||
|
||||
export interface DataRetention {
|
||||
enabled: boolean;
|
||||
infiniteDataRetention?: boolean;
|
||||
value?: number;
|
||||
unit?: string;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,13 @@ export * from './mappings';
|
|||
|
||||
export * from './templates';
|
||||
|
||||
export type { EnhancedDataStreamFromEs, Health, DataStream, DataStreamIndex } from './data_streams';
|
||||
export type {
|
||||
EnhancedDataStreamFromEs,
|
||||
Health,
|
||||
DataStream,
|
||||
DataStreamIndex,
|
||||
DataRetention,
|
||||
} from './data_streams';
|
||||
|
||||
export * from './component_templates';
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataRetention, DataStream } from './data_streams';
|
||||
import { IndexSettings } from './indices';
|
||||
import { Aliases } from './aliases';
|
||||
import { Mappings } from './mappings';
|
||||
|
@ -18,6 +19,7 @@ export interface TemplateSerialized {
|
|||
settings?: IndexSettings;
|
||||
aliases?: Aliases;
|
||||
mappings?: Mappings;
|
||||
lifecycle?: DataStream['lifecycle'];
|
||||
};
|
||||
composed_of?: string[];
|
||||
version?: number;
|
||||
|
@ -40,6 +42,7 @@ export interface TemplateDeserialized {
|
|||
aliases?: Aliases;
|
||||
mappings?: Mappings;
|
||||
};
|
||||
lifecycle?: DataRetention;
|
||||
composedOf?: string[]; // Composable template only
|
||||
version?: number;
|
||||
priority?: number; // Composable template only
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { EuiFilterSelectItem, EuiPopover, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { UseField } from '../../../../../shared_imports';
|
||||
import { UseField } from '../../../../shared_imports';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
export type { CommonWizardSteps } from './components';
|
||||
|
||||
export {
|
||||
TabAliases,
|
||||
TabMappings,
|
||||
|
@ -15,3 +16,6 @@ export {
|
|||
StepSettingsContainer,
|
||||
TemplateContentIndicator,
|
||||
} from './components';
|
||||
|
||||
export { UnitField } from './fields/unit_field';
|
||||
export { timeUnits } from '../../constants/time_units';
|
||||
|
|
|
@ -26,7 +26,10 @@ import {
|
|||
Field,
|
||||
Forms,
|
||||
JsonEditorField,
|
||||
NumericField,
|
||||
} from '../../../../shared_imports';
|
||||
import { UnitField, timeUnits } from '../../shared';
|
||||
import { DataRetention } from '../../../../../common';
|
||||
import { documentationService } from '../../../services/documentation';
|
||||
import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas';
|
||||
|
||||
|
@ -112,6 +115,19 @@ function getFieldsMeta(esDocsBase: string) {
|
|||
}),
|
||||
testSubject: 'versionField',
|
||||
},
|
||||
dataRetention: {
|
||||
title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.dataRetentionTitle', {
|
||||
defaultMessage: 'Data retention',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.idxMgmt.templateForm.stepLogistics.dataRetentionDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Data will be kept at least this long before being automatically deleted.',
|
||||
}
|
||||
),
|
||||
unitTestSubject: 'unitDataRetentionField',
|
||||
},
|
||||
allowAutoCreate: {
|
||||
title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.allowAutoCreateTitle', {
|
||||
defaultMessage: 'Allow auto create',
|
||||
|
@ -177,9 +193,18 @@ export const StepLogistics: React.FunctionComponent<Props> = React.memo(
|
|||
getFormData,
|
||||
} = form;
|
||||
|
||||
const [{ addMeta }] = useFormData<{ addMeta: boolean }>({
|
||||
const [{ addMeta, doCreateDataStream, lifecycle }] = useFormData<{
|
||||
addMeta: boolean;
|
||||
lifecycle: DataRetention;
|
||||
doCreateDataStream: boolean;
|
||||
}>({
|
||||
form,
|
||||
watch: 'addMeta',
|
||||
watch: [
|
||||
'addMeta',
|
||||
'lifecycle.enabled',
|
||||
'lifecycle.infiniteDataRetention',
|
||||
'doCreateDataStream',
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -198,8 +223,16 @@ export const StepLogistics: React.FunctionComponent<Props> = React.memo(
|
|||
});
|
||||
}, [onChange, isFormValid, validate, getFormData]);
|
||||
|
||||
const { name, indexPatterns, createDataStream, order, priority, version, allowAutoCreate } =
|
||||
getFieldsMeta(documentationService.getEsDocsBase());
|
||||
const {
|
||||
name,
|
||||
indexPatterns,
|
||||
createDataStream,
|
||||
order,
|
||||
priority,
|
||||
version,
|
||||
dataRetention,
|
||||
allowAutoCreate,
|
||||
} = getFieldsMeta(documentationService.getEsDocsBase());
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -272,6 +305,61 @@ export const StepLogistics: React.FunctionComponent<Props> = React.memo(
|
|||
</FormRow>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Since data stream and data retention are settings that are only allowed for non legacy,
|
||||
we only need to check if data stream is set to true to show the data retention.
|
||||
*/}
|
||||
{doCreateDataStream && (
|
||||
<FormRow
|
||||
title={dataRetention.title}
|
||||
description={
|
||||
<>
|
||||
{dataRetention.description}
|
||||
<EuiSpacer size="m" />
|
||||
<UseField
|
||||
path="lifecycle.enabled"
|
||||
componentProps={{ 'data-test-subj': 'dataRetentionToggle' }}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{lifecycle?.enabled && (
|
||||
<UseField
|
||||
path="lifecycle.value"
|
||||
component={NumericField}
|
||||
labelAppend={
|
||||
<UseField
|
||||
path="lifecycle.infiniteDataRetention"
|
||||
data-test-subj="infiniteDataRetentionToggle"
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
compressed: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
disabled: lifecycle?.infiniteDataRetention,
|
||||
'data-test-subj': 'valueDataRetentionField',
|
||||
min: 1,
|
||||
append: (
|
||||
<UnitField
|
||||
path="lifecycle.unit"
|
||||
options={timeUnits}
|
||||
disabled={lifecycle?.infiniteDataRetention}
|
||||
euiFieldProps={{
|
||||
'data-test-subj': 'unitDataRetentionField',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</FormRow>
|
||||
)}
|
||||
|
||||
{/* Order */}
|
||||
{isLegacy && (
|
||||
<FormRow title={order.title} description={order.description}>
|
||||
|
|
|
@ -27,9 +27,11 @@ import { serializers } from '../../../../shared_imports';
|
|||
import { serializeLegacyTemplate, serializeTemplate } from '../../../../../common/lib';
|
||||
import { TemplateDeserialized, getTemplateParameter, Aliases } from '../../../../../common';
|
||||
import { SimulateTemplate } from '../../index_templates';
|
||||
import { getLifecycleValue } from '../../../lib/data_streams';
|
||||
import { WizardSection } from '../template_form';
|
||||
|
||||
const { stripEmptyFields } = serializers;
|
||||
const INFINITE_AS_ICON = true;
|
||||
|
||||
const NoneDescriptionText = () => (
|
||||
<FormattedMessage
|
||||
|
@ -87,6 +89,7 @@ export const StepReview: React.FunctionComponent<Props> = React.memo(
|
|||
indexPatterns,
|
||||
version,
|
||||
order,
|
||||
template: indexTemplate,
|
||||
priority,
|
||||
allowAutoCreate,
|
||||
composedOf,
|
||||
|
@ -109,6 +112,7 @@ export const StepReview: React.FunctionComponent<Props> = React.memo(
|
|||
const serializedMappings = getTemplateParameter(serializedTemplate, 'mappings');
|
||||
const serializedSettings = getTemplateParameter(serializedTemplate, 'settings');
|
||||
const serializedAliases = getTemplateParameter(serializedTemplate, 'aliases');
|
||||
const serializedLifecycle = (indexTemplate as TemplateDeserialized)?.lifecycle;
|
||||
|
||||
const numIndexPatterns = indexPatterns!.length;
|
||||
|
||||
|
@ -274,6 +278,20 @@ export const StepReview: React.FunctionComponent<Props> = React.memo(
|
|||
{getDescriptionText(serializedAliases)}
|
||||
</EuiDescriptionListDescription>
|
||||
|
||||
{isLegacy !== true && serializedLifecycle?.enabled && (
|
||||
<>
|
||||
<EuiDescriptionListTitle data-test-subj="lifecycleTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepReview.summaryTab.lifecycleLabel"
|
||||
defaultMessage="Data retention"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="lifecycleValue">
|
||||
{getLifecycleValue(serializedLifecycle, INFINITE_AS_ICON)}
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Metadata (optional) */}
|
||||
{isLegacy !== true && _meta && (
|
||||
<>
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from '../shared';
|
||||
import { documentationService } from '../../services/documentation';
|
||||
import { SectionError } from '../section_error';
|
||||
import { serializeAsESLifecycle } from '../../../../common/lib/data_stream_serialization';
|
||||
import {
|
||||
SimulateTemplateFlyoutContent,
|
||||
SimulateTemplateProps,
|
||||
|
@ -190,6 +191,9 @@ export const TemplateForm = ({
|
|||
if (Object.keys(outputTemplate.template).length === 0) {
|
||||
delete outputTemplate.template;
|
||||
}
|
||||
if (outputTemplate.lifecycle) {
|
||||
delete outputTemplate.lifecycle;
|
||||
}
|
||||
}
|
||||
|
||||
return outputTemplate;
|
||||
|
@ -206,10 +210,13 @@ export const TemplateForm = ({
|
|||
settings: wizardData.settings,
|
||||
mappings: wizardData.mappings,
|
||||
aliases: wizardData.aliases,
|
||||
lifecycle: wizardData.logistics.lifecycle
|
||||
? serializeAsESLifecycle(wizardData.logistics.lifecycle)
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
return cleanupTemplateObject(outputTemplate);
|
||||
return cleanupTemplateObject(outputTemplate as TemplateDeserialized);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
|
|
@ -158,6 +158,85 @@ export const schemas: Record<string, FormSchema> = {
|
|||
}),
|
||||
formatters: [toInt],
|
||||
},
|
||||
|
||||
'lifecycle.enabled': {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.enableDataRetentionLabel', {
|
||||
defaultMessage: 'Enable data retention',
|
||||
}),
|
||||
defaultValue: false,
|
||||
},
|
||||
'lifecycle.infiniteDataRetention': {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.infiniteDataRetentionLabel', {
|
||||
defaultMessage: 'Keep data indefinitely',
|
||||
}),
|
||||
defaultValue: false,
|
||||
},
|
||||
'lifecycle.value': {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate(
|
||||
'xpack.idxMgmt.templateForm.stepLogistics.fieldDataRetentionValueLabel',
|
||||
{
|
||||
defaultMessage: 'Data Retention',
|
||||
}
|
||||
),
|
||||
formatters: [toInt],
|
||||
validations: [
|
||||
{
|
||||
validator: ({ value, formData }) => {
|
||||
// If infiniteRetentionPeriod is set, we dont need to validate the data retention field
|
||||
if (formData['lifecycle.infiniteDataRetention']) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'xpack.idxMgmt.templateForm.stepLogistics.dataRetentionFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'A data retention value is required.',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (value <= 0) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'xpack.idxMgmt.templateForm.stepLogistics.dataRetentionFieldNonNegativeError',
|
||||
{
|
||||
defaultMessage: `A positive value is required.`,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (value % 1 !== 0) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'xpack.idxMgmt.templateForm.stepLogistics.dataRetentionFieldDecimalError',
|
||||
{
|
||||
defaultMessage: `The value should be an integer number.`,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'lifecycle.unit': {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate(
|
||||
'xpack.idxMgmt.templateForm.stepLogistics.fieldDataRetentionUnitLabel',
|
||||
{
|
||||
defaultMessage: 'Time unit',
|
||||
}
|
||||
),
|
||||
defaultValue: 'd',
|
||||
},
|
||||
|
||||
allowAutoCreate: {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldAllowAutoCreateLabel', {
|
||||
|
|
|
@ -42,7 +42,7 @@ import { splitSizeAndUnits, DataStream } from '../../../../../../common';
|
|||
import { timeUnits } from '../../../../constants/time_units';
|
||||
import { isDSLWithILMIndices } from '../../../../lib/data_streams';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
import { UnitField } from './unit_field';
|
||||
import { UnitField } from '../../../../components/shared';
|
||||
import { updateDataRetention } from '../../../../services/api';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -20,6 +20,8 @@ import {
|
|||
EuiCodeBlock,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { serializeAsESLifecycle } from '../../../../../../../common/lib/data_stream_serialization';
|
||||
import { getLifecycleValue } from '../../../../../lib/data_streams';
|
||||
import { TemplateDeserialized } from '../../../../../../../common';
|
||||
import { ILM_PAGES_POLICY_EDIT } from '../../../../../constants';
|
||||
import { useIlmLocator } from '../../../../../services/use_ilm_locator';
|
||||
|
@ -28,6 +30,7 @@ interface Props {
|
|||
templateDetails: TemplateDeserialized;
|
||||
}
|
||||
|
||||
const INFINITE_AS_ICON = true;
|
||||
const i18nTexts = {
|
||||
yes: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.yesDescriptionText', {
|
||||
defaultMessage: 'Yes',
|
||||
|
@ -194,6 +197,24 @@ export const TabSummary: React.FunctionComponent<Props> = ({ templateDetails })
|
|||
{version || version === 0 ? version : i18nTexts.none}
|
||||
</EuiDescriptionListDescription>
|
||||
|
||||
{/* Data retention */}
|
||||
{hasDatastream && templateDetails?.lifecycle && (
|
||||
<>
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateDetails.summaryTab.lifecycleDescriptionListTitle"
|
||||
defaultMessage="Data retention"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{getLifecycleValue(
|
||||
serializeAsESLifecycle(templateDetails.lifecycle),
|
||||
INFINITE_AS_ICON
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Allow auto create */}
|
||||
{isLegacy !== true && (
|
||||
<>
|
||||
|
|
|
@ -19,6 +19,12 @@ export const templateSchema = schema.object({
|
|||
settings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
mappings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
lifecycle: schema.maybe(
|
||||
schema.object({
|
||||
enabled: schema.boolean(),
|
||||
data_retention: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
composedOf: schema.maybe(schema.arrayOf(schema.string())),
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { getRandomString, getRandomNumber } from '@kbn/test-jest-helpers';
|
||||
import { TemplateDeserialized, TemplateType, TemplateListItem } from '../../common';
|
||||
import { IndexSettings, Aliases, Mappings, DataStream } from '../../common/types';
|
||||
|
||||
const objHasProperties = (obj?: Record<string, any>): boolean => {
|
||||
return obj === undefined || Object.keys(obj).length === 0 ? false : true;
|
||||
|
@ -17,7 +18,7 @@ export const getComposableTemplate = ({
|
|||
version = getRandomNumber(),
|
||||
priority = getRandomNumber(),
|
||||
indexPatterns = [],
|
||||
template: { settings, aliases, mappings } = {},
|
||||
template: { settings, aliases, mappings, lifecycle } = {},
|
||||
hasDatastream = false,
|
||||
isLegacy = false,
|
||||
type = 'default',
|
||||
|
@ -27,6 +28,12 @@ export const getComposableTemplate = ({
|
|||
isLegacy?: boolean;
|
||||
type?: TemplateType;
|
||||
hasDatastream: boolean;
|
||||
template?: {
|
||||
settings?: IndexSettings;
|
||||
aliases?: Aliases;
|
||||
mappings?: Mappings;
|
||||
lifecycle?: DataStream['lifecycle'];
|
||||
};
|
||||
}
|
||||
> = {}): TemplateDeserialized => {
|
||||
const indexTemplate = {
|
||||
|
@ -39,6 +46,7 @@ export const getComposableTemplate = ({
|
|||
aliases,
|
||||
mappings,
|
||||
settings,
|
||||
lifecycle,
|
||||
},
|
||||
_kbnMeta: {
|
||||
type,
|
||||
|
|
|
@ -30,9 +30,26 @@ export default function ({ getService }) {
|
|||
after(() => Promise.all([cleanUpEsResources(), cleanUpTemplates()]));
|
||||
|
||||
describe('get all', () => {
|
||||
const templateName = `template-${getRandomString()}`;
|
||||
const indexTemplate = getTemplatePayload(templateName, [getRandomString()]);
|
||||
const legacyTemplate = getTemplatePayload(templateName, [getRandomString()], true);
|
||||
const indexTemplate = getTemplatePayload(`template-${getRandomString()}`, [
|
||||
getRandomString(),
|
||||
]);
|
||||
const legacyTemplate = getTemplatePayload(
|
||||
`template-${getRandomString()}`,
|
||||
[getRandomString()],
|
||||
true
|
||||
);
|
||||
const tmpTemplate = getTemplatePayload(`template-${getRandomString()}`, [getRandomString()]);
|
||||
const indexTemplateWithLifecycle = {
|
||||
...tmpTemplate,
|
||||
dataStream: {},
|
||||
template: {
|
||||
...tmpTemplate.template,
|
||||
lifecycle: {
|
||||
enabled: true,
|
||||
data_retention: '10d',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const res1 = await createTemplate(indexTemplate);
|
||||
|
@ -44,6 +61,11 @@ export default function ({ getService }) {
|
|||
if (res2.status !== 200) {
|
||||
throw new Error(res2.body.message);
|
||||
}
|
||||
|
||||
const res3 = await createTemplate(indexTemplateWithLifecycle);
|
||||
if (res3.status !== 200) {
|
||||
throw new Error(res3.body.message);
|
||||
}
|
||||
});
|
||||
|
||||
it('should list all the index templates with the expected parameters', async () => {
|
||||
|
@ -107,6 +129,36 @@ export default function ({ getService }) {
|
|||
].sort();
|
||||
|
||||
expect(Object.keys(legacyTemplateFound).sort()).to.eql(expectedLegacyKeys);
|
||||
|
||||
// Index template with lifecycle
|
||||
const templateWithLifecycle = allTemplates.templates.find(
|
||||
(template) => template.name === indexTemplateWithLifecycle.name
|
||||
);
|
||||
|
||||
if (!templateWithLifecycle) {
|
||||
throw new Error(
|
||||
`Index template with lifecycle "${
|
||||
indexTemplateWithLifecycle.name
|
||||
}" not found in ${JSON.stringify(allTemplates.templates, null, 2)}`
|
||||
);
|
||||
}
|
||||
|
||||
const expectedWithLifecycleKeys = [
|
||||
'name',
|
||||
'indexPatterns',
|
||||
'lifecycle',
|
||||
'hasSettings',
|
||||
'hasAliases',
|
||||
'hasMappings',
|
||||
'ilmPolicy',
|
||||
'priority',
|
||||
'composedOf',
|
||||
'dataStream',
|
||||
'version',
|
||||
'_kbnMeta',
|
||||
].sort();
|
||||
|
||||
expect(Object.keys(templateWithLifecycle).sort()).to.eql(expectedWithLifecycleKeys);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const pageObjects = getPageObjects(['common', 'indexManagement', 'header']);
|
||||
const log = getService('log');
|
||||
const security = getService('security');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
const INDEX_TEMPLATE_NAME = `test-index-template`;
|
||||
|
||||
describe('Create index template', function () {
|
||||
before(async () => {
|
||||
await log.debug('Navigating to the index templates tab');
|
||||
await security.testUser.setRoles(['index_management_user']);
|
||||
await pageObjects.common.navigateToApp('indexManagement');
|
||||
// Navigate to the data streams tab
|
||||
await pageObjects.indexManagement.changeTabs('templatesTab');
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
// Click create template button
|
||||
await testSubjects.click('createTemplateButton');
|
||||
});
|
||||
|
||||
it('can create an index template with data retention', async () => {
|
||||
// Complete required fields from step 1
|
||||
await testSubjects.setValue('nameField', INDEX_TEMPLATE_NAME);
|
||||
await testSubjects.setValue('indexPatternsField', 'test-1');
|
||||
// Enable data stream
|
||||
await testSubjects.click('dataStreamField > input');
|
||||
// Enable data retention
|
||||
await testSubjects.click('dataRetentionToggle > input');
|
||||
// Set the retention to 7 hours
|
||||
await testSubjects.setValue('valueDataRetentionField', '7');
|
||||
await testSubjects.click('show-filters-button');
|
||||
await testSubjects.click('filter-option-h');
|
||||
// Navigate to the last step of the wizard
|
||||
await testSubjects.click('nextButton');
|
||||
await testSubjects.click('nextButton');
|
||||
await testSubjects.click('nextButton');
|
||||
await testSubjects.click('nextButton');
|
||||
await testSubjects.click('nextButton');
|
||||
|
||||
expect(await testSubjects.getVisibleText('lifecycleValue')).to.be('7 hours');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ loadTestFile }: FtrProviderContext) => {
|
||||
describe('Index Management: index templates tab', function () {
|
||||
loadTestFile(require.resolve('./create_index_template'));
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue