From 40e524bd0973734e8de424d9f975158ea7ddd275 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Tue, 31 Oct 2023 17:09:53 +0100 Subject: [PATCH] [Index Management] Support managed_by field and fix never delete data logic (#169064) --- .../helpers/test_subjects.ts | 5 + .../home/data_streams_tab.helpers.ts | 3 + .../home/data_streams_tab.test.ts | 157 +++++++++++++++--- .../common/lib/data_stream_serialization.ts | 21 ++- .../common/types/data_streams.ts | 21 ++- .../public/application/lib/data_streams.tsx | 39 +++++ .../data_stream_detail_panel.tsx | 133 ++++++++++++--- .../data_stream_detail_panel/index.ts | 2 +- .../data_stream_table/data_stream_table.tsx | 36 +++- .../edit_data_retention_modal.tsx | 131 ++++++++++++--- .../public/application/services/api.ts | 21 ++- .../api/data_streams/register_get_route.ts | 2 + .../api/data_streams/register_put_route.ts | 17 +- .../index_management/data_streams.ts | 22 +++ .../data_streams_tab/data_streams_tab.ts | 18 ++ .../index_management/data_streams.ts | 18 ++ 16 files changed, 548 insertions(+), 98 deletions(-) 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 0f4ce20f0c156..52ac8313ef4d0 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,6 +102,11 @@ export type TestSubjects = | 'filter-option-h' | 'infiniteRetentionPeriod.input' | 'saveButton' + | 'dsIsFullyManagedByILM' + | 'someIndicesAreManagedByILMCallout' + | 'viewIlmPolicyLink' + | 'viewAllIndicesLink' + | 'dataRetentionEnabledField.input' | 'enrichPoliciesInsuficientPrivileges' | '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 3a6add88c2840..631b3e838f8ff 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 @@ -264,9 +264,12 @@ export const createDataStreamPayload = (dataStream: Partial): DataSt { name: 'indexName', uuid: 'indexId', + preferILM: false, + managedBy: 'Data stream lifecycle', }, ], generation: 1, + nextGenerationManagedBy: 'Data stream lifecycle', health: 'green', indexTemplateName: 'indexTemplate', storageSize: '1b', 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 dc4c228322bbd..e78ad9d167f87 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 @@ -446,6 +446,32 @@ describe('Data Streams tab', () => { ); }); + test('can disable lifecycle', async () => { + const { + actions: { clickNameAt, clickEditDataRetentionButton }, + } = testBed; + + await clickNameAt(0); + + clickEditDataRetentionButton(); + + httpRequestsMockHelpers.setEditDataRetentionResponse('dataStream1', { + success: true, + }); + + testBed.form.toggleEuiSwitch('dataRetentionEnabledField.input'); + + await act(async () => { + testBed.find('saveButton').simulate('click'); + }); + testBed.component.update(); + + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/data_streams/dataStream1/data_retention`, + expect.objectContaining({ body: JSON.stringify({ enabled: false }) }) + ); + }); + test('allows to set infinite retention period', async () => { const { actions: { clickNameAt, clickEditDataRetentionButton }, @@ -499,6 +525,110 @@ describe('Data Streams tab', () => { expect(findDetailPanelDataRetentionDetail().exists()).toBeTruthy(); }); }); + + describe('shows all possible states according to who manages the data stream', () => { + const ds1 = createDataStreamPayload({ + name: 'dataStream1', + nextGenerationManagedBy: 'Index Lifecycle Management', + lifecycle: undefined, + indices: [ + { + managedBy: 'Index Lifecycle Management', + name: 'indexName', + uuid: 'indexId', + preferILM: true, + }, + ], + }); + + const ds2 = createDataStreamPayload({ + name: 'dataStream2', + nextGenerationManagedBy: 'Data stream lifecycle', + lifecycle: { + enabled: true, + data_retention: '7d', + }, + indices: [ + { + managedBy: 'Index Lifecycle Management', + name: 'indexName1', + uuid: 'indexId1', + preferILM: true, + }, + { + managedBy: 'Index Lifecycle Management', + name: 'indexName2', + uuid: 'indexId2', + preferILM: true, + }, + { + managedBy: 'Index Lifecycle Management', + name: 'indexName3', + uuid: 'indexId3', + preferILM: true, + }, + { + managedBy: 'Index Lifecycle Management', + name: 'indexName4', + uuid: 'indexId4', + preferILM: true, + }, + ], + }); + + beforeEach(async () => { + const { setLoadDataStreamsResponse } = httpRequestsMockHelpers; + + setLoadDataStreamsResponse([ds1, ds2]); + + testBed = await setup(httpSetup, { + history: createMemoryHistory(), + url: urlServiceMock, + }); + + await act(async () => { + testBed.actions.goToDataStreamsList(); + }); + testBed.component.update(); + }); + + test('when fully managed by ILM, user cannot edit data retention', async () => { + const { setLoadDataStreamResponse } = httpRequestsMockHelpers; + + setLoadDataStreamResponse(ds1.name, ds1); + + const { actions, find, exists } = testBed; + + await actions.clickNameAt(0); + expect(find('dataRetentionDetail').text()).toBe('Disabled'); + + // There should be a warning that the data stream is fully managed by ILM + expect(exists('dsIsFullyManagedByILM')).toBe(true); + + // Edit data retention button should not be visible + testBed.find('manageDataStreamButton').simulate('click'); + expect(exists('editDataRetentionButton')).toBe(false); + }); + + test('when partially managed by dsl but has backing indices managed by ILM should show a warning', async () => { + const { setLoadDataStreamResponse } = httpRequestsMockHelpers; + + setLoadDataStreamResponse(ds2.name, ds2); + + const { actions, find, exists } = testBed; + + await actions.clickNameAt(1); + expect(find('dataRetentionDetail').text()).toBe('7d'); + + actions.clickEditDataRetentionButton(); + + // There should be a warning that the data stream is managed by DSL + // but the backing indices that are managed by ILM wont be affected. + expect(exists('someIndicesAreManagedByILMCallout')).toBe(true); + expect(exists('viewIlmPolicyLink')).toBe(true); + expect(exists('viewAllIndicesLink')).toBe(true); + }); + }); }); describe('when there are special characters', () => { @@ -569,33 +699,6 @@ describe('Data Streams tab', () => { expect(findDetailPanelIlmPolicyLink().prop('href')).toBe('/test/my_ilm_policy'); }); - test('with ILM updating data retention should be disabled', async () => { - const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; - - const dataStreamForDetailPanel = createDataStreamPayload({ - name: 'dataStream1', - ilmPolicyName: 'my_ilm_policy', - }); - - setLoadDataStreamsResponse([dataStreamForDetailPanel]); - setLoadDataStreamResponse(dataStreamForDetailPanel.name, dataStreamForDetailPanel); - - testBed = await setup(httpSetup, { - history: createMemoryHistory(), - url: urlServiceMock, - }); - await act(async () => { - testBed.actions.goToDataStreamsList(); - }); - testBed.component.update(); - - const { actions } = testBed; - await actions.clickNameAt(0); - - testBed.find('manageDataStreamButton').simulate('click'); - expect(testBed.find('editDataRetentionButton').exists()).toBeFalsy(); - }); - test('with an ILM url locator and no ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; diff --git a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts index b9743727a1756..d7bd1d9e92f95 100644 --- a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts @@ -23,16 +23,28 @@ export function deserializeDataStream(dataStreamFromEs: EnhancedDataStreamFromEs privileges, hidden, lifecycle, + next_generation_managed_by: nextGenerationManagedBy, } = dataStreamFromEs; return { name, timeStampField, indices: indices.map( - // eslint-disable-next-line @typescript-eslint/naming-convention - ({ index_name, index_uuid }: { index_name: string; index_uuid: string }) => ({ - name: index_name, - uuid: index_uuid, + ({ + index_name: indexName, + index_uuid: indexUuid, + prefer_ilm: preferILM, + managed_by: managedBy, + }: { + index_name: string; + index_uuid: string; + prefer_ilm: boolean; + managed_by: string; + }) => ({ + name: indexName, + uuid: indexUuid, + preferILM, + managedBy, }) ), generation, @@ -46,6 +58,7 @@ export function deserializeDataStream(dataStreamFromEs: EnhancedDataStreamFromEs privileges, hidden, lifecycle, + nextGenerationManagedBy, }; } 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 f0bd12d96fde5..80a4be29ee924 100644 --- a/x-pack/plugins/index_management/common/types/data_streams.ts +++ b/x-pack/plugins/index_management/common/types/data_streams.ts @@ -28,23 +28,27 @@ type Privileges = PrivilegesFromEs; export type HealthFromEs = 'GREEN' | 'YELLOW' | 'RED'; +export interface DataStreamIndexFromEs { + index_name: string; + index_uuid: string; + prefer_ilm: boolean; + managed_by: string; +} + +export type Health = 'green' | 'yellow' | 'red'; + export interface EnhancedDataStreamFromEs extends IndicesDataStream { store_size?: IndicesDataStreamsStatsDataStreamsStatsItem['store_size']; store_size_bytes?: IndicesDataStreamsStatsDataStreamsStatsItem['store_size_bytes']; maximum_timestamp?: IndicesDataStreamsStatsDataStreamsStatsItem['maximum_timestamp']; + indices: DataStreamIndexFromEs[]; + next_generation_managed_by: string; privileges: { delete_index: boolean; manage_data_stream_lifecycle: boolean; }; } -export interface DataStreamIndexFromEs { - index_name: string; - index_uuid: string; -} - -export type Health = 'green' | 'yellow' | 'red'; - export interface DataStream { name: string; timeStampField: TimestampField; @@ -59,6 +63,7 @@ export interface DataStream { _meta?: Metadata; privileges: Privileges; hidden: boolean; + nextGenerationManagedBy: string; lifecycle?: IndicesDataLifecycleWithRollover & { enabled?: boolean; }; @@ -67,4 +72,6 @@ export interface DataStream { export interface DataStreamIndex { name: string; uuid: string; + preferILM: boolean; + managedBy: string; } 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 ad068dde91a22..90b6001d3d949 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 @@ -76,3 +76,42 @@ export const getLifecycleValue = ( return lifecycle?.data_retention; }; + +export const isDataStreamFullyManagedByILM = (dataStream?: DataStream | null) => { + return ( + dataStream?.nextGenerationManagedBy?.toLowerCase() === 'index lifecycle management' && + dataStream?.indices?.every( + (index) => index.managedBy.toLowerCase() === 'index lifecycle management' + ) + ); +}; + +export const isDataStreamFullyManagedByDSL = (dataStream?: DataStream | null) => { + return ( + dataStream?.nextGenerationManagedBy?.toLowerCase() === 'data stream lifecycle' && + dataStream?.indices?.every((index) => index.managedBy.toLowerCase() === 'data stream lifecycle') + ); +}; + +export const isDSLWithILMIndices = (dataStream?: DataStream | null) => { + if (dataStream?.nextGenerationManagedBy?.toLowerCase() === 'data stream lifecycle') { + const ilmIndices = dataStream?.indices?.filter( + (index) => index.managedBy.toLowerCase() === 'index lifecycle management' + ); + const dslIndices = dataStream?.indices?.filter( + (index) => index.managedBy.toLowerCase() === 'data stream lifecycle' + ); + + // When there arent any ILM indices, there's no need to show anything. + if (!ilmIndices?.length) { + return; + } + + return { + ilmIndices, + dslIndices, + }; + } + + return; +}; 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 96449e6de5238..8784c7f7257b3 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 @@ -22,11 +22,15 @@ import { EuiFlyoutHeader, EuiIconTip, EuiLink, + EuiTextColor, EuiTitle, EuiIcon, + EuiToolTip, EuiPopover, EuiContextMenu, EuiContextMenuPanelDescriptor, + EuiCallOut, + EuiSpacer, } from '@elastic/eui'; import { DiscoverLink } from '../../../../lib/discover_link'; @@ -39,6 +43,10 @@ import { EditDataRetentionModal } from '../edit_data_retention_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; import { getIndexListUri, getTemplateDetailsLink } from '../../../../services/routing'; import { ILM_PAGES_POLICY_EDIT } from '../../../../constants'; +import { + isDataStreamFullyManagedByILM, + isDataStreamFullyManagedByDSL, +} from '../../../../lib/data_streams'; import { useAppContext } from '../../../../app_context'; import { DataStreamsBadges } from '../data_stream_badges'; import { useIlmLocator } from '../../../../services/use_ilm_locator'; @@ -99,6 +107,16 @@ interface Props { onClose: (shouldReload?: boolean) => void; } +export const ConditionalWrap = ({ + condition, + wrap, + children, +}: { + condition: boolean; + wrap: (wrappedChildren: React.ReactNode) => JSX.Element; + children: JSX.Element; +}): JSX.Element => (condition ? wrap(children) : children); + export const DataStreamDetailPanel: React.FunctionComponent = ({ dataStreamName, onClose, @@ -111,6 +129,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, dataStream?.ilmPolicyName); const { history } = useAppContext(); + let indicesLink; let content; @@ -154,14 +173,38 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ defaultMessage: 'Index lifecycle policy', }), toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyToolTip', { - defaultMessage: `The index lifecycle policy that manages the data in the data stream.`, + defaultMessage: `The index lifecycle policy that manages the data in the data stream. `, }), - content: ilmPolicyLink ? ( - - {ilmPolicyName} - + content: isDataStreamFullyManagedByDSL(dataStream) ? ( + + <> + {ilmPolicyLink ? ( + + {ilmPolicyName} + + ) : ( + ilmPolicyName + )} + + ) : ( - ilmPolicyName + <> + {ilmPolicyLink ? ( + + {ilmPolicyName} + + ) : ( + ilmPolicyName + )} + ), dataTestSubj: 'ilmPolicyDetail', }); @@ -170,6 +213,14 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ return managementDetails; }; + indicesLink = ( + + {indices.length} + + ); + const defaultDetails = [ { name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.healthTitle', { @@ -216,16 +267,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.indicesToolTip', { defaultMessage: `The data stream's current backing indices.`, }), - content: ( - - {indices.length} - - ), + content: indicesLink, dataTestSubj: 'indicesDetail', }, { @@ -271,9 +313,16 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ defaultMessage: 'Data retention', }), toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.dataRetentionToolTip', { - defaultMessage: 'The amount of time to retain the data in the data stream.', + defaultMessage: `Data is kept at least this long before being automatically deleted. The data retention value only applies to the data managed directly by the data stream. If some data is subject to an index lifecycle management policy, then the data retention value set for the data stream doesn't apply to that data.`, }), - content: getLifecycleValue(lifecycle), + content: ( + {children}} + > + <>{getLifecycleValue(lifecycle)} + + ), dataTestSubj: 'dataRetentionDetail', }, ]; @@ -281,7 +330,43 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ const managementDetails = getManagementDetails(); const details = [...defaultDetails, ...managementDetails]; - content = ; + content = ( + <> + {isDataStreamFullyManagedByILM(dataStream) && ( + <> + +

+ + + + ), + }} + /> +

+
+ + + + )} + + + + ); } const closePopover = () => { @@ -310,7 +395,8 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ defaultMessage: 'Data stream options', }), items: [ - ...(!dataStream?.ilmPolicyName && dataStream?.privileges?.manage_data_stream_lifecycle + ...(!isDataStreamFullyManagedByILM(dataStream) && + dataStream?.privileges?.manage_data_stream_lifecycle ? [ { key: 'editDataRetention', @@ -364,7 +450,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ /> )} - {isEditingDataRetention && ( + {isEditingDataRetention && dataStream && ( { if (data && data?.hasUpdatedDataRetention) { @@ -373,8 +459,9 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ setIsEditingDataRetention(false); } }} - dataStreamName={dataStreamName} - lifecycle={dataStream?.lifecycle} + ilmPolicyName={dataStream?.ilmPolicyName} + ilmPolicyLink={ilmPolicyLink} + dataStream={dataStream} /> )} diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts index dbff310fe947c..d6c33961ab03c 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { DataStreamDetailPanel } from './data_stream_detail_panel'; +export { DataStreamDetailPanel, ConditionalWrap } from './data_stream_detail_panel'; 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 1bf8886ccbf73..97293c9a5f13b 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 @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, Fragment } from 'react'; +import React, { useState, Fragment, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -15,6 +15,7 @@ import { EuiLink, EuiIcon, EuiToolTip, + EuiTextColor, } from '@elastic/eui'; import { ScopedHistory } from '@kbn/core/public'; @@ -27,6 +28,12 @@ import { DataHealth } from '../../../../components'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; import { DataStreamsBadges } from '../data_stream_badges'; +import { ConditionalWrap } from '../data_stream_detail_panel'; +import { isDataStreamFullyManagedByILM } from '../../../../lib/data_streams'; + +interface TableDataStream extends DataStream { + isDataStreamFullyManagedByILM: boolean; +} interface Props { dataStreams?: DataStream[]; @@ -49,7 +56,14 @@ export const DataStreamTable: React.FunctionComponent = ({ const [dataStreamsToDelete, setDataStreamsToDelete] = useState([]); const { config } = useAppContext(); - const columns: Array> = []; + const data = useMemo(() => { + return (dataStreams || []).map((dataStream) => ({ + ...dataStream, + isDataStreamFullyManagedByILM: isDataStreamFullyManagedByILM(dataStream), + })); + }, [dataStreams]); + + const columns: Array> = []; columns.push({ field: 'name', @@ -137,8 +151,7 @@ export const DataStreamTable: React.FunctionComponent = ({ name: ( @@ -151,7 +164,14 @@ export const DataStreamTable: React.FunctionComponent = ({ ), truncateText: true, sortable: true, - render: (lifecycle: DataStream['lifecycle']) => getLifecycleValue(lifecycle, INFINITE_AS_ICON), + render: (lifecycle: DataStream['lifecycle'], dataStream) => ( + {children}} + > + <>{getLifecycleValue(lifecycle, INFINITE_AS_ICON)} + + ), }); columns.push({ @@ -235,8 +255,8 @@ export const DataStreamTable: React.FunctionComponent = ({ <> {dataStreamsToDelete && dataStreamsToDelete.length > 0 ? ( { - if (data && data.hasDeletedDataStreams) { + onClose={(res) => { + if (res && res.hasDeletedDataStreams) { reload(); } else { setDataStreamsToDelete([]); @@ -246,7 +266,7 @@ export const DataStreamTable: React.FunctionComponent = ({ /> ) : null} void; } @@ -146,13 +151,83 @@ const configurationFormSchema: FormSchema = { } ), }, + dataRetentionEnabled: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + label: i18n.translate( + 'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.dataRetentionEnabledField', + { + defaultMessage: 'Enable data retention', + } + ), + }, }; -export const EditDataRetentionModal: React.FunctionComponent = ({ - lifecycle, +interface MixedIndicesCalloutProps { + history: ScopedHistory; + ilmPolicyLink: string; + ilmPolicyName?: string; + dataStreamName: string; +} + +const MixedIndicesCallout = ({ + ilmPolicyLink, + ilmPolicyName, dataStreamName, + history, +}: MixedIndicesCalloutProps) => { + return ( + +

+ + {ilmPolicyName} + + ), + viewAllIndicesLink: ( + + + + ), + }} + /> +

+
+ ); +}; + +export const EditDataRetentionModal: React.FunctionComponent = ({ + dataStream, + ilmPolicyName, + ilmPolicyLink, onClose, }) => { + const lifecycle = dataStream?.lifecycle; + const dataStreamName = dataStream?.name as string; + + const { history } = useAppContext(); + const dslWithIlmIndices = isDSLWithILMIndices(dataStream); const { size, unit } = splitSizeAndUnits(lifecycle?.data_retention as string); const { services: { notificationService }, @@ -162,6 +237,7 @@ export const EditDataRetentionModal: React.FunctionComponent = ({ defaultValue: { dataRetention: size, timeUnit: unit || 'd', + dataRetentionEnabled: lifecycle?.enabled, // 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. @@ -184,7 +260,11 @@ export const EditDataRetentionModal: React.FunctionComponent = ({ if (responseData) { const successMessage = i18n.translate( 'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.successDataRetentionNotification', - { defaultMessage: 'Data retention updated' } + { + defaultMessage: + 'Data retention {disabledDataRetention, plural, one { disabled } other { updated } }', + values: { disabledDataRetention: !data.dataRetentionEnabled ? 1 : 0 }, + } ); notificationService.showSuccessToast(successMessage); @@ -214,21 +294,29 @@ export const EditDataRetentionModal: React.FunctionComponent = ({ {' '} - - - {i18n.translate( - 'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.techPreviewLabel', - { - defaultMessage: 'Technical preview', - } - )} - - + /> + {dslWithIlmIndices && ( + <> + + + + )} + + + = ({ componentProps={{ fullWidth: false, euiFieldProps: { - disabled: formData.infiniteRetentionPeriod, + disabled: formData.infiniteRetentionPeriod || !formData.dataRetentionEnabled, 'data-test-subj': `dataRetentionValue`, min: 1, append: ( = ({ path="infiniteRetentionPeriod" component={ToggleField} data-test-subj="infiniteRetentionPeriod" + componentProps={{ + euiFieldProps: { + disabled: !formData.dataRetentionEnabled, + }, + }} /> diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index 30c8339840018..de911383cf26c 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -85,14 +85,27 @@ export async function deleteDataStreams(dataStreams: string[]) { export async function updateDataRetention( name: string, - data: { dataRetention: string; timeUnit: string; infiniteRetentionPeriod: boolean } + data: { + dataRetention: string; + timeUnit: string; + infiniteRetentionPeriod: boolean; + dataRetentionEnabled: boolean; + } ) { + let body; + + if (!data.dataRetentionEnabled) { + body = { enabled: false }; + } else { + body = data.infiniteRetentionPeriod + ? {} + : { dataRetention: `${data.dataRetention}${data.timeUnit}` }; + } + return sendRequest({ path: `${API_BASE_PATH}/data_streams/${encodeURIComponent(name)}/data_retention`, method: 'put', - body: data.infiniteRetentionPeriod - ? {} - : { dataRetention: `${data.dataRetention}${data.timeUnit}` }, + body, }); } diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts index 8ca301e6bee07..a3aaf4192432b 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts @@ -28,6 +28,7 @@ const enhanceDataStreams = ({ dataStreamsPrivileges?: SecurityHasPrivilegesResponse; }): EnhancedDataStreamFromEs[] => { return dataStreams.map((dataStream) => { + // @ts-expect-error @elastic/elasticsearch next_generation_managed_by prop is still not in the ES types const enhancedDataStream: EnhancedDataStreamFromEs = { ...dataStream, privileges: { @@ -143,6 +144,7 @@ export function registerGetOneRoute({ router, lib: { handleEsError }, config }: if (dataStreams[0]) { let dataStreamsPrivileges; + if (config.isSecurityEnabled()) { dataStreamsPrivileges = await getDataStreamsPrivileges(client, [dataStreams[0].name]); } diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_put_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_put_route.ts index cba52ce9101d0..536f6d6287453 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_put_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_put_route.ts @@ -16,6 +16,7 @@ export function registerPutDataRetention({ router, lib: { handleEsError } }: Rou }); const bodySchema = schema.object({ dataRetention: schema.maybe(schema.string()), + enabled: schema.maybe(schema.boolean()), }); router.put( @@ -25,15 +26,21 @@ export function registerPutDataRetention({ router, lib: { handleEsError } }: Rou }, async (context, request, response) => { const { name } = request.params as TypeOf; - const { dataRetention } = request.body as TypeOf; + const { dataRetention, enabled } = request.body as TypeOf; const { client } = (await context.core).elasticsearch; try { - await client.asCurrentUser.indices.putDataLifecycle({ - name, - data_retention: dataRetention, - }); + // Only when enabled is explicitly set to false, we delete the data retention policy. + if (enabled === false) { + await client.asCurrentUser.indices.deleteDataLifecycle({ name }); + } else { + // Otherwise, we create or update the data retention policy. + await client.asCurrentUser.indices.putDataLifecycle({ + name, + data_retention: dataRetention, + }); + } return response.ok({ body: { success: true } }); } catch (error) { 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 e0373a405cde7..6ed1ec9ed1c4c 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 @@ -128,8 +128,11 @@ export default function ({ getService }: FtrProviderContext) { { name: indexName, uuid, + preferILM: true, + managedBy: 'Data stream lifecycle', }, ], + nextGenerationManagedBy: 'Data stream lifecycle', generation: 1, health: 'yellow', indexTemplateName: testDataStreamName, @@ -167,12 +170,15 @@ export default function ({ getService }: FtrProviderContext) { indices: [ { name: indexName, + managedBy: 'Data stream lifecycle', + preferILM: true, uuid, }, ], generation: 1, health: 'yellow', indexTemplateName: testDataStreamName, + nextGenerationManagedBy: 'Data stream lifecycle', maxTimeStamp: 0, hidden: false, lifecycle: { @@ -202,12 +208,15 @@ export default function ({ getService }: FtrProviderContext) { indices: [ { name: indexName, + managedBy: 'Data stream lifecycle', + preferILM: true, uuid, }, ], generation: 1, health: 'yellow', indexTemplateName: testDataStreamName, + nextGenerationManagedBy: 'Data stream lifecycle', maxTimeStamp: 0, hidden: false, lifecycle: { @@ -244,6 +253,19 @@ export default function ({ getService }: FtrProviderContext) { expect(body).to.eql({ success: true }); }); + + it('can disable lifecycle for a given policy', async () => { + const { body } = await supertest + .put(`${API_BASE_PATH}/data_streams/${testDataStreamName}/data_retention`) + .set('kbn-xsrf', 'xxx') + .send({ enabled: false }) + .expect(200); + + expect(body).to.eql({ success: true }); + + const datastream = await getDatastream(testDataStreamName); + expect(datastream.lifecycle).to.be(undefined); + }); }); describe('Delete', () => { 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 eea731575f8f3..9d3da94fead4a 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 @@ -109,5 +109,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const successToast = await toasts.getToastElement(1); expect(await successToast.getVisibleText()).to.contain('Data retention updated'); }); + + it('allows to disable data retention', async () => { + // Open details flyout + await pageObjects.indexManagement.clickDataStreamAt(0); + // Open the edit retention dialog + await testSubjects.click('manageDataStreamButton'); + await testSubjects.click('editDataRetentionButton'); + + // Disable infinite retention + await testSubjects.click('dataRetentionEnabledField > input'); + + // Submit the form + await testSubjects.click('saveButton'); + + // Expect to see a success toast + const successToast = await toasts.getToastElement(1); + expect(await successToast.getVisibleText()).to.contain('Data retention disabled'); + }); }); }; diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/data_streams.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/data_streams.ts index 88cdcba00d181..bbf55e359d360 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/data_streams.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/data_streams.ts @@ -119,5 +119,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const successToast = await toasts.getToastElement(1); expect(await successToast.getVisibleText()).to.contain('Data retention updated'); }); + + it('allows to disable data retention', async () => { + // Open details flyout + await pageObjects.indexManagement.clickDataStreamAt(0); + // Open the edit retention dialog + await testSubjects.click('manageDataStreamButton'); + await testSubjects.click('editDataRetentionButton'); + + // Disable infinite retention + await testSubjects.click('dataRetentionEnabledField > input'); + + // Submit the form + await testSubjects.click('saveButton'); + + // Expect to see a success toast + const successToast = await toasts.getToastElement(1); + expect(await successToast.getVisibleText()).to.contain('Data retention disabled'); + }); }); };