mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Index Management] Add data retention to component template UI (#170837)
This commit is contained in:
parent
9b29b1898e
commit
02ccb7788a
14 changed files with 267 additions and 8 deletions
|
@ -8,15 +8,18 @@
|
|||
import { IndexSettings } from './indices';
|
||||
import { Aliases } from './aliases';
|
||||
import { Mappings } from './mappings';
|
||||
import { DataStream, DataRetention } from '.';
|
||||
|
||||
export interface ComponentTemplateSerialized {
|
||||
template: {
|
||||
settings?: IndexSettings;
|
||||
aliases?: Aliases;
|
||||
mappings?: Mappings;
|
||||
lifecycle?: DataStream['lifecycle'];
|
||||
};
|
||||
version?: number;
|
||||
_meta?: { [key: string]: any };
|
||||
lifecycle?: DataRetention;
|
||||
}
|
||||
|
||||
export interface ComponentTemplateDeserialized extends ComponentTemplateSerialized {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { breadcrumbService, IndexManagementBreadcrumb } from '../../../../servic
|
|||
import { setupEnvironment } from './helpers';
|
||||
import { API_BASE_PATH } from './helpers/constants';
|
||||
import { setup, ComponentTemplateCreateTestBed } from './helpers/component_template_create.helpers';
|
||||
import { serializeAsESLifecycle } from '../../../../../../common/lib/data_stream_serialization';
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public', () => {
|
||||
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
|
||||
|
@ -98,6 +99,19 @@ describe('<ComponentTemplateCreate />', () => {
|
|||
expect(exists('metaEditor')).toBe(true);
|
||||
});
|
||||
|
||||
test('should toggle the data retention field', async () => {
|
||||
const { exists, component, form } = testBed;
|
||||
|
||||
expect(exists('valueDataRetentionField')).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
form.toggleEuiSwitch('dataRetentionToggle.input');
|
||||
});
|
||||
component.update();
|
||||
|
||||
expect(exists('valueDataRetentionField')).toBe(true);
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
test('should require a name', async () => {
|
||||
const { form, actions, component, find } = testBed;
|
||||
|
@ -120,6 +134,11 @@ describe('<ComponentTemplateCreate />', () => {
|
|||
const COMPONENT_TEMPLATE_NAME = 'comp-1';
|
||||
const SETTINGS = { number_of_shards: 1 };
|
||||
const ALIASES = { my_alias: {} };
|
||||
const LIFECYCLE = {
|
||||
enabled: true,
|
||||
value: 2,
|
||||
unit: 'd',
|
||||
};
|
||||
|
||||
const BOOLEAN_MAPPING_FIELD = {
|
||||
name: 'boolean_datatype',
|
||||
|
@ -136,7 +155,10 @@ describe('<ComponentTemplateCreate />', () => {
|
|||
component.update();
|
||||
|
||||
// Complete step 1 (logistics)
|
||||
await actions.completeStepLogistics({ name: COMPONENT_TEMPLATE_NAME });
|
||||
await actions.completeStepLogistics({
|
||||
name: COMPONENT_TEMPLATE_NAME,
|
||||
lifecycle: LIFECYCLE,
|
||||
});
|
||||
|
||||
// Complete step 2 (index settings)
|
||||
await actions.completeStepSettings(SETTINGS);
|
||||
|
@ -199,6 +221,7 @@ describe('<ComponentTemplateCreate />', () => {
|
|||
},
|
||||
},
|
||||
aliases: ALIASES,
|
||||
lifecycle: serializeAsESLifecycle(LIFECYCLE),
|
||||
},
|
||||
_kbnMeta: { usedBy: [], isManaged: false },
|
||||
}),
|
||||
|
|
|
@ -19,6 +19,7 @@ const COMPONENT_TEMPLATE: ComponentTemplateDeserialized = {
|
|||
mappings: { properties: { ip_address: { type: 'ip' } } },
|
||||
aliases: { mydata: {} },
|
||||
settings: { number_of_shards: 1 },
|
||||
lifecycle: { enabled: true, data_retention: '4d' },
|
||||
},
|
||||
version: 1,
|
||||
_meta: { description: 'component template test' },
|
||||
|
@ -72,6 +73,7 @@ describe('<ComponentTemplateDetails />', () => {
|
|||
expect(exists('summaryTabContent.usedByTitle')).toBe(true);
|
||||
expect(exists('summaryTabContent.versionTitle')).toBe(true);
|
||||
expect(exists('summaryTabContent.metaTitle')).toBe(true);
|
||||
expect(exists('summaryTabContent.dataRetentionTitle')).toBe(true);
|
||||
|
||||
// [Settings tab] Navigate to tab and verify content
|
||||
act(() => {
|
||||
|
|
|
@ -78,6 +78,7 @@ export type ComponentTemplateDetailsTestSubjects =
|
|||
| 'summaryTabContent.usedByTitle'
|
||||
| 'summaryTabContent.versionTitle'
|
||||
| 'summaryTabContent.metaTitle'
|
||||
| 'summaryTabContent.dataRetentionTitle'
|
||||
| 'notInUseCallout'
|
||||
| 'aliasesTabContent'
|
||||
| 'noAliasesCallout'
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { TestBed } from '@kbn/test-jest-helpers';
|
||||
import { DataRetention } from '../../../../../../../common';
|
||||
|
||||
interface MappingField {
|
||||
name: string;
|
||||
|
@ -52,11 +53,28 @@ export const getFormActions = (testBed: TestBed) => {
|
|||
.simulate('click');
|
||||
};
|
||||
|
||||
const completeStepLogistics = async ({ name }: { name: string }) => {
|
||||
const completeStepLogistics = async ({
|
||||
name,
|
||||
lifecycle,
|
||||
}: {
|
||||
name: string;
|
||||
lifecycle: DataRetention;
|
||||
}) => {
|
||||
const { form, component } = testBed;
|
||||
// Add name field
|
||||
form.setInputValue('nameField.input', name);
|
||||
|
||||
if (lifecycle && lifecycle.enabled) {
|
||||
act(() => {
|
||||
form.toggleEuiSwitch('dataRetentionToggle.input');
|
||||
});
|
||||
component.update();
|
||||
|
||||
act(() => {
|
||||
form.setInputValue('valueDataRetentionField', String(lifecycle.value));
|
||||
});
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
clickNextButton();
|
||||
});
|
||||
|
@ -164,6 +182,8 @@ export type ComponentTemplateFormTestSubjects =
|
|||
| 'stepReview.content'
|
||||
| 'stepReview.summaryTab'
|
||||
| 'stepReview.requestTab'
|
||||
| 'valueDataRetentionField'
|
||||
| 'dataRetentionToggle.input'
|
||||
| 'versionField'
|
||||
| 'aliasesEditor'
|
||||
| 'mappingsEditor'
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { getLifecycleValue } from '../../../lib/data_streams';
|
||||
import { ComponentTemplateDeserialized } from '../shared_imports';
|
||||
import { useComponentTemplatesContext } from '../component_templates_context';
|
||||
|
||||
|
@ -27,13 +28,15 @@ interface Props {
|
|||
showCallToAction?: boolean;
|
||||
}
|
||||
|
||||
const INFINITE_AS_ICON = true;
|
||||
|
||||
export const TabSummary: React.FunctionComponent<Props> = ({
|
||||
componentTemplateDetails,
|
||||
showCallToAction,
|
||||
}) => {
|
||||
const { getUrlForApp } = useComponentTemplatesContext();
|
||||
|
||||
const { version, _meta, _kbnMeta } = componentTemplateDetails;
|
||||
const { version, _meta, _kbnMeta, template } = componentTemplateDetails;
|
||||
|
||||
const { usedBy } = _kbnMeta;
|
||||
const templateIsInUse = usedBy.length > 0;
|
||||
|
@ -118,6 +121,20 @@ export const TabSummary: React.FunctionComponent<Props> = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
{template.lifecycle && (
|
||||
<>
|
||||
<EuiDescriptionListTitle data-test-subj="dataRetentionTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateDetails.summaryTab.dataRetentionDescriptionListTitle"
|
||||
defaultMessage="Data retention"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{getLifecycleValue(template.lifecycle, INFINITE_AS_ICON)}
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Version (optional) */}
|
||||
{typeof version !== 'undefined' && (
|
||||
<>
|
||||
|
|
|
@ -19,6 +19,10 @@ import {
|
|||
StepMappingsContainer,
|
||||
StepAliasesContainer,
|
||||
} from '../../shared_imports';
|
||||
import {
|
||||
serializeAsESLifecycle,
|
||||
deserializeESLifecycle,
|
||||
} from '../../../../../../common/lib/data_stream_serialization';
|
||||
import { useComponentTemplatesContext } from '../../component_templates_context';
|
||||
import { StepLogisticsContainer, StepReviewContainer } from './steps';
|
||||
|
||||
|
@ -96,14 +100,17 @@ export const ComponentTemplateForm = ({
|
|||
onStepChange,
|
||||
}: Props) => {
|
||||
const {
|
||||
template: { settings, mappings, aliases },
|
||||
template: { settings, mappings, aliases, lifecycle },
|
||||
...logistics
|
||||
} = defaultValue;
|
||||
|
||||
const { documentation } = useComponentTemplatesContext();
|
||||
|
||||
const wizardDefaultValue: WizardContent = {
|
||||
logistics,
|
||||
logistics: {
|
||||
...logistics,
|
||||
...(lifecycle ? { lifecycle: deserializeESLifecycle(lifecycle) } : {}),
|
||||
},
|
||||
settings,
|
||||
mappings,
|
||||
aliases,
|
||||
|
@ -162,6 +169,10 @@ export const ComponentTemplateForm = ({
|
|||
delete outputTemplate.template.aliases;
|
||||
}
|
||||
|
||||
if (outputTemplate.lifecycle) {
|
||||
delete outputTemplate.lifecycle;
|
||||
}
|
||||
|
||||
return outputTemplate;
|
||||
};
|
||||
|
||||
|
@ -177,9 +188,14 @@ export const ComponentTemplateForm = ({
|
|||
settings: wizardData.settings,
|
||||
mappings: wizardData.mappings,
|
||||
aliases: wizardData.aliases,
|
||||
lifecycle: wizardData.logistics.lifecycle
|
||||
? serializeAsESLifecycle(wizardData.logistics.lifecycle)
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
return cleanupComponentTemplateObject(outputComponentTemplate);
|
||||
return cleanupComponentTemplateObject(
|
||||
outputComponentTemplate as ComponentTemplateDeserialized
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
|
|
@ -24,8 +24,12 @@ import {
|
|||
getFormRow,
|
||||
Field,
|
||||
Forms,
|
||||
NumericField,
|
||||
JsonEditorField,
|
||||
useFormData,
|
||||
} from '../../../shared_imports';
|
||||
import { DataRetention } from '../../../../../../../common';
|
||||
import { UnitField, timeUnits } from '../../../../shared';
|
||||
import { useComponentTemplatesContext } from '../../../component_templates_context';
|
||||
import { logisticsFormSchema } from './step_logistics_schema';
|
||||
|
||||
|
@ -48,6 +52,13 @@ export const StepLogistics: React.FunctionComponent<Props> = React.memo(
|
|||
|
||||
const { isValid: isFormValid, submit, getFormData, subscribe } = form;
|
||||
|
||||
const [{ lifecycle }] = useFormData<{
|
||||
lifecycle: DataRetention;
|
||||
}>({
|
||||
form,
|
||||
watch: ['lifecycle.enabled', 'lifecycle.infiniteDataRetention'],
|
||||
});
|
||||
|
||||
const { documentation } = useComponentTemplatesContext();
|
||||
|
||||
const [isMetaVisible, setIsMetaVisible] = useState<boolean>(
|
||||
|
@ -134,6 +145,64 @@ export const StepLogistics: React.FunctionComponent<Props> = React.memo(
|
|||
/>
|
||||
</FormRow>
|
||||
|
||||
{/* Data retention field */}
|
||||
<FormRow
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.dataRetentionTitle"
|
||||
defaultMessage="Data retention"
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepLogistics.dataRetentionDescription"
|
||||
defaultMessage="Data will be kept at least this long before being automatically deleted."
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* version field */}
|
||||
<FormRow
|
||||
title={
|
||||
|
|
|
@ -58,6 +58,89 @@ export const logisticsFormSchema: FormSchema = {
|
|||
},
|
||||
],
|
||||
},
|
||||
'lifecycle.enabled': {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
label: i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplateForm.stepLogistics.enableDataRetentionLabel',
|
||||
{
|
||||
defaultMessage: 'Enable data retention',
|
||||
}
|
||||
),
|
||||
defaultValue: false,
|
||||
},
|
||||
'lifecycle.infiniteDataRetention': {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
label: i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplateForm.stepLogistics.infiniteDataRetentionLabel',
|
||||
{
|
||||
defaultMessage: 'Keep data indefinitely',
|
||||
}
|
||||
),
|
||||
defaultValue: false,
|
||||
},
|
||||
'lifecycle.value': {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplateForm.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.dataStreamsDetailsPanel.stepLogistics.dataRetentionFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'A data retention value is required.',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (value <= 0) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.stepLogistics.dataRetentionFieldNonNegativeError',
|
||||
{
|
||||
defaultMessage: `A positive value is required.`,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (value % 1 !== 0) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.stepLogistics.dataRetentionFieldDecimalError',
|
||||
{
|
||||
defaultMessage: `The value should be an integer number.`,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'lifecycle.unit': {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate(
|
||||
'xpack.idxMgmt.componentTemplateForm.stepLogistics.fieldDataRetentionUnitLabel',
|
||||
{
|
||||
defaultMessage: 'Time unit',
|
||||
}
|
||||
),
|
||||
defaultValue: 'd',
|
||||
},
|
||||
version: {
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.versionFieldLabel', {
|
||||
|
|
|
@ -28,7 +28,9 @@ import {
|
|||
serializeComponentTemplate,
|
||||
} from '../../../shared_imports';
|
||||
import { MANAGED_BY_FLEET } from '../../../constants';
|
||||
import { getLifecycleValue } from '../../../../../lib/data_streams';
|
||||
|
||||
const INFINITE_AS_ICON = true;
|
||||
const { stripEmptyFields } = serializers;
|
||||
|
||||
const getDescriptionText = (data: any) => {
|
||||
|
@ -123,6 +125,17 @@ export const StepReview: React.FunctionComponent<Props> = React.memo(
|
|||
<EuiDescriptionListDescription>
|
||||
{getDescriptionText(serializedTemplate?.aliases)}
|
||||
</EuiDescriptionListDescription>
|
||||
|
||||
{/* Data retention */}
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.componentTemplateForm.stepReview.summaryTab.dataRetentionLabel"
|
||||
defaultMessage="Data retention"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{getLifecycleValue(serializedTemplate?.lifecycle, INFINITE_AS_ICON)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
{isFleetDatastreamsVisible && dataStreams && (
|
||||
|
|
|
@ -41,12 +41,14 @@ export {
|
|||
useForm,
|
||||
Form,
|
||||
getUseField,
|
||||
useFormData,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
|
||||
export {
|
||||
getFormRow,
|
||||
Field,
|
||||
JsonEditorField,
|
||||
NumericField,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
|
||||
export { isJSON } from '@kbn/es-ui-shared-plugin/static/validators/string';
|
||||
|
|
|
@ -33,9 +33,9 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep
|
|||
const { index_templates: indexTemplates } =
|
||||
await client.asCurrentUser.indices.getIndexTemplate();
|
||||
|
||||
const body = componentTemplates.map((componentTemplate: ComponentTemplateFromEs) => {
|
||||
const body = componentTemplates.map((componentTemplate) => {
|
||||
const deserializedComponentTemplateListItem = deserializeComponentTemplateList(
|
||||
componentTemplate,
|
||||
componentTemplate as ComponentTemplateFromEs,
|
||||
// @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns
|
||||
indexTemplates
|
||||
);
|
||||
|
|
|
@ -13,6 +13,12 @@ export const componentTemplateSchema = 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()),
|
||||
})
|
||||
),
|
||||
}),
|
||||
version: schema.maybe(schema.number()),
|
||||
_meta: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
|
|
|
@ -147,6 +147,10 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
},
|
||||
},
|
||||
lifecycle: {
|
||||
enabled: true,
|
||||
data_retention: '2d',
|
||||
},
|
||||
},
|
||||
_meta: {
|
||||
description: 'set number of shards to one',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue