diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index b6656c0dae354..10e94bb2f718c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -102,4 +102,5 @@ export type TestSubjects = | 'filter-option-h' | 'infiniteRetentionPeriod.input' | 'saveButton' + | 'dataRetentionDetail' | 'createIndexSaveButton'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts index 6629502498c74..3a6add88c2840 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -278,6 +278,7 @@ export const createDataStreamPayload = (dataStream: Partial): DataSt }, hidden: false, lifecycle: { + enabled: true, data_retention: '7d', }, ...dataStream, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index c40823509d640..bbb8d924fcd02 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -338,6 +338,55 @@ describe('Data Streams tab', () => { }); describe('update data retention', () => { + test('Should show disabled or infinite retention period accordingly in table and flyout', async () => { + const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; + + const ds1 = createDataStreamPayload({ + name: 'dataStream1', + lifecycle: { + enabled: false, + }, + }); + const ds2 = createDataStreamPayload({ + name: 'dataStream2', + lifecycle: { + enabled: true, + }, + }); + + setLoadDataStreamsResponse([ds1, ds2]); + setLoadDataStreamResponse(ds1.name, ds1); + + testBed = await setup(httpSetup, { + history: createMemoryHistory(), + url: urlServiceMock, + }); + await act(async () => { + testBed.actions.goToDataStreamsList(); + }); + testBed.component.update(); + + const { actions, find, table } = testBed; + const { tableCellsValues } = table.getMetaData('dataStreamTable'); + + expect(tableCellsValues).toEqual([ + ['', 'dataStream1', 'green', '1', 'Disabled', 'Delete'], + ['', 'dataStream2', 'green', '1', '', 'Delete'], + ]); + + await actions.clickNameAt(0); + expect(find('dataRetentionDetail').text()).toBe('Disabled'); + + await act(async () => { + testBed.find('closeDetailsButton').simulate('click'); + }); + testBed.component.update(); + + setLoadDataStreamResponse(ds2.name, ds2); + await actions.clickNameAt(1); + expect(find('dataRetentionDetail').text()).toBe('Keep data indefinitely'); + }); + test('can set data retention period', async () => { const { actions: { clickNameAt, clickEditDataRetentionButton }, diff --git a/x-pack/plugins/index_management/common/types/data_streams.ts b/x-pack/plugins/index_management/common/types/data_streams.ts index 8e2e5c3368ac3..f0bd12d96fde5 100644 --- a/x-pack/plugins/index_management/common/types/data_streams.ts +++ b/x-pack/plugins/index_management/common/types/data_streams.ts @@ -59,7 +59,9 @@ export interface DataStream { _meta?: Metadata; privileges: Privileges; hidden: boolean; - lifecycle?: IndicesDataLifecycleWithRollover; + lifecycle?: IndicesDataLifecycleWithRollover & { + enabled?: boolean; + }; } export interface DataStreamIndex { diff --git a/x-pack/plugins/index_management/public/application/lib/data_streams.test.tsx b/x-pack/plugins/index_management/public/application/lib/data_streams.test.tsx new file mode 100644 index 0000000000000..a15cbaefbd0fa --- /dev/null +++ b/x-pack/plugins/index_management/public/application/lib/data_streams.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getLifecycleValue } from './data_streams'; + +describe('Data stream helpers', () => { + describe('getLifecycleValue', () => { + it('Knows when it should be marked as disabled', () => { + expect( + getLifecycleValue({ + enabled: false, + }) + ).toBe('Disabled'); + + expect(getLifecycleValue()).toBe('Disabled'); + }); + + it('knows when it should be marked as infinite', () => { + expect( + getLifecycleValue({ + enabled: true, + }) + ).toBe('Keep data indefinitely'); + }); + + it('knows when it has a defined data retention period', () => { + expect( + getLifecycleValue({ + enabled: true, + data_retention: '2d', + }) + ).toBe('2d'); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/lib/data_streams.tsx b/x-pack/plugins/index_management/public/application/lib/data_streams.tsx index 8cf45050855b0..ad068dde91a22 100644 --- a/x-pack/plugins/index_management/public/application/lib/data_streams.tsx +++ b/x-pack/plugins/index_management/public/application/lib/data_streams.tsx @@ -5,6 +5,10 @@ * 2.0. */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiToolTip } from '@elastic/eui'; + import { DataStream } from '../../../common'; export const isFleetManaged = (dataStream: DataStream): boolean => { @@ -38,3 +42,37 @@ export const isSelectedDataStreamHidden = ( ?.hidden ); }; + +export const getLifecycleValue = ( + lifecycle?: DataStream['lifecycle'], + inifniteAsIcon?: boolean +) => { + if (!lifecycle?.enabled) { + return i18n.translate('xpack.idxMgmt.dataStreamList.dataRetentionDisabled', { + defaultMessage: 'Disabled', + }); + } else if (!lifecycle?.data_retention) { + const infiniteDataRetention = i18n.translate( + 'xpack.idxMgmt.dataStreamList.dataRetentionInfinite', + { + defaultMessage: 'Keep data indefinitely', + } + ); + + if (inifniteAsIcon) { + return ( + + + + ); + } + + return infiniteDataRetention; + } + + return lifecycle?.data_retention; +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index d04976d157406..96449e6de5238 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -30,6 +30,7 @@ import { } from '@elastic/eui'; import { DiscoverLink } from '../../../../lib/discover_link'; +import { getLifecycleValue } from '../../../../lib/data_streams'; import { SectionLoading, reactRouterNavigate } from '../../../../../shared_imports'; import { SectionError, Error, DataHealth } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; @@ -147,19 +148,6 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ const getManagementDetails = () => { const managementDetails = []; - if (lifecycle?.data_retention) { - managementDetails.push({ - name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.dataRetentionTitle', { - defaultMessage: 'Data retention', - }), - toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.dataRetentionToolTip', { - defaultMessage: 'The amount of time to retain the data in the data stream.', - }), - content: lifecycle.data_retention, - dataTestSubj: 'dataRetentionDetail', - }); - } - if (ilmPolicyName) { managementDetails.push({ name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyTitle', { @@ -278,6 +266,16 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ ), dataTestSubj: 'indexTemplateDetail', }, + { + name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.dataRetentionTitle', { + defaultMessage: 'Data retention', + }), + toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.dataRetentionToolTip', { + defaultMessage: 'The amount of time to retain the data in the data stream.', + }), + content: getLifecycleValue(lifecycle), + dataTestSubj: 'dataRetentionDetail', + }, ]; const managementDetails = getManagementDetails(); @@ -376,7 +374,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ } }} dataStreamName={dataStreamName} - dataRetention={dataStream?.lifecycle?.data_retention as string} + lifecycle={dataStream?.lifecycle} /> )} diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx index 485f1db9c06c9..0bf6aa68347de 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx @@ -19,6 +19,7 @@ import { import { ScopedHistory } from '@kbn/core/public'; import { DataStream } from '../../../../../../common/types'; +import { getLifecycleValue } from '../../../../lib/data_streams'; import { UseRequestResponse, reactRouterNavigate } from '../../../../../shared_imports'; import { getDataStreamDetailsLink, getIndexListUri } from '../../../../services/routing'; import { DataHealth } from '../../../../components'; @@ -34,6 +35,8 @@ interface Props { filters?: string; } +const INFINITE_AS_ICON = true; + export const DataStreamTable: React.FunctionComponent = ({ dataStreams, reload, @@ -144,7 +147,7 @@ export const DataStreamTable: React.FunctionComponent = ({ ), truncateText: true, sortable: true, - render: (lifecycle: DataStream['lifecycle']) => lifecycle?.data_retention, + render: (lifecycle: DataStream['lifecycle']) => getLifecycleValue(lifecycle, INFINITE_AS_ICON), }); columns.push({ diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/edit_data_retention_modal/edit_data_retention_modal.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/edit_data_retention_modal/edit_data_retention_modal.tsx index e33aaae7a9073..11e51a83b8e99 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/edit_data_retention_modal/edit_data_retention_modal.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/edit_data_retention_modal/edit_data_retention_modal.tsx @@ -35,13 +35,13 @@ import { } from '../../../../../shared_imports'; import { documentationService } from '../../../../services/documentation'; -import { splitSizeAndUnits } from '../../../../../../common'; +import { splitSizeAndUnits, DataStream } from '../../../../../../common'; import { useAppContext } from '../../../../app_context'; import { UnitField } from './unit_field'; import { updateDataRetention } from '../../../../services/api'; interface Props { - dataRetention: string; + lifecycle: DataStream['lifecycle']; dataStreamName: string; onClose: (data?: { hasUpdatedDataRetention: boolean }) => void; } @@ -83,33 +83,6 @@ export const timeUnits = [ } ), }, - { - value: 'ms', - text: i18n.translate( - 'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.timeUnits.millisecondsLabel', - { - defaultMessage: 'milliseconds', - } - ), - }, - { - value: 'micros', - text: i18n.translate( - 'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.timeUnits.microsecondsLabel', - { - defaultMessage: 'microseconds', - } - ), - }, - { - value: 'nanos', - text: i18n.translate( - 'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.timeUnits.nanosecondsLabel', - { - defaultMessage: 'nanoseconds', - } - ), - }, ]; const configurationFormSchema: FormSchema = { @@ -124,7 +97,12 @@ const configurationFormSchema: FormSchema = { formatters: [fieldFormatters.toInt], validations: [ { - validator: ({ value }) => { + validator: ({ value, formData }) => { + // If infiniteRetentionPeriod is set, we dont need to validate the data retention field + if (formData.infiniteRetentionPeriod) { + return undefined; + } + if (!value) { return { message: i18n.translate( @@ -171,11 +149,11 @@ const configurationFormSchema: FormSchema = { }; export const EditDataRetentionModal: React.FunctionComponent = ({ - dataRetention, + lifecycle, dataStreamName, onClose, }) => { - const { size, unit } = splitSizeAndUnits(dataRetention); + const { size, unit } = splitSizeAndUnits(lifecycle?.data_retention as string); const { services: { notificationService }, } = useAppContext(); @@ -184,7 +162,10 @@ export const EditDataRetentionModal: React.FunctionComponent = ({ defaultValue: { dataRetention: size, timeUnit: unit || 'd', - infiniteRetentionPeriod: !dataRetention, + // When data retention is not set and lifecycle is enabled, is the only scenario in + // which data retention will be infinite. If lifecycle isnt set or is not enabled, we + // dont have inifinite data retention. + infiniteRetentionPeriod: lifecycle?.enabled && !lifecycle?.data_retention, }, schema: configurationFormSchema, id: 'editDataRetentionForm', diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts index a76df2127a1fb..e0373a405cde7 100644 --- a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts +++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts @@ -31,6 +31,10 @@ export default function ({ getService }: FtrProviderContext) { }, }, }, + lifecycle: { + // @ts-expect-error @elastic/elasticsearch enabled prop is not typed yet + enabled: true, + }, }, data_stream: {}, }, @@ -85,8 +89,7 @@ export default function ({ getService }: FtrProviderContext) { expect(typeof storageSizeBytes).to.be('number'); }; - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/168021 - describe.skip('Data streams', function () { + describe('Data streams', function () { describe('Get', () => { const testDataStreamName = 'test-data-stream'; diff --git a/x-pack/test/functional/apps/index_management/data_streams_tab/data_streams_tab.ts b/x-pack/test/functional/apps/index_management/data_streams_tab/data_streams_tab.ts index c801c40c7e067..eea731575f8f3 100644 --- a/x-pack/test/functional/apps/index_management/data_streams_tab/data_streams_tab.ts +++ b/x-pack/test/functional/apps/index_management/data_streams_tab/data_streams_tab.ts @@ -12,8 +12,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'indexManagement', 'header']); const toasts = getService('toasts'); const log = getService('log'); - const dataStreams = getService('dataStreams'); const browser = getService('browser'); + const es = getService('es'); const security = getService('security'); const testSubjects = getService('testSubjects'); @@ -23,15 +23,32 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { await log.debug('Creating required data stream'); try { - await dataStreams.createDataStream( - TEST_DS_NAME, - { - '@timestamp': { - type: 'date', + await es.indices.putIndexTemplate({ + name: `${TEST_DS_NAME}_index_template`, + index_patterns: [TEST_DS_NAME], + data_stream: {}, + _meta: { + description: `Template for ${TEST_DS_NAME} testing index`, + }, + template: { + settings: { mode: undefined }, + mappings: { + properties: { + '@timestamp': { + type: 'date', + }, + }, + }, + lifecycle: { + // @ts-expect-error @elastic/elasticsearch enabled prop is not typed yet + enabled: true, }, }, - false - ); + }); + + await es.indices.createDataStream({ + name: TEST_DS_NAME, + }); } catch (e) { log.debug('[Setup error] Error creating test data stream'); throw e; @@ -49,7 +66,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await log.debug('Cleaning up created data stream'); try { - await dataStreams.deleteDataStream(TEST_DS_NAME); + await es.indices.deleteDataStream({ name: TEST_DS_NAME }); + await es.indices.deleteIndexTemplate({ + name: `${TEST_DS_NAME}_index_template`, + }); } catch (e) { log.debug('[Teardown error] Error deleting test data stream'); throw e;