mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Index Management] Support managed_by field and fix never delete data logic (#169064)
This commit is contained in:
parent
7ad80a0884
commit
40e524bd09
16 changed files with 548 additions and 98 deletions
|
@ -102,6 +102,11 @@ export type TestSubjects =
|
|||
| 'filter-option-h'
|
||||
| 'infiniteRetentionPeriod.input'
|
||||
| 'saveButton'
|
||||
| 'dsIsFullyManagedByILM'
|
||||
| 'someIndicesAreManagedByILMCallout'
|
||||
| 'viewIlmPolicyLink'
|
||||
| 'viewAllIndicesLink'
|
||||
| 'dataRetentionEnabledField.input'
|
||||
| 'enrichPoliciesInsuficientPrivileges'
|
||||
| 'dataRetentionDetail'
|
||||
| 'createIndexSaveButton';
|
||||
|
|
|
@ -264,9 +264,12 @@ export const createDataStreamPayload = (dataStream: Partial<DataStream>): DataSt
|
|||
{
|
||||
name: 'indexName',
|
||||
uuid: 'indexId',
|
||||
preferILM: false,
|
||||
managedBy: 'Data stream lifecycle',
|
||||
},
|
||||
],
|
||||
generation: 1,
|
||||
nextGenerationManagedBy: 'Data stream lifecycle',
|
||||
health: 'green',
|
||||
indexTemplateName: 'indexTemplate',
|
||||
storageSize: '1b',
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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<Props> = ({
|
||||
dataStreamName,
|
||||
onClose,
|
||||
|
@ -111,6 +129,7 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
|
||||
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<Props> = ({
|
|||
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 ? (
|
||||
<EuiLink data-test-subj={'ilmPolicyLink'} href={ilmPolicyLink}>
|
||||
{ilmPolicyName}
|
||||
</EuiLink>
|
||||
content: isDataStreamFullyManagedByDSL(dataStream) ? (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyToolTipWarning',
|
||||
{
|
||||
defaultMessage: `This data stream is not currently being managed by the ILM policy.`,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{ilmPolicyLink ? (
|
||||
<EuiLink data-test-subj={'ilmPolicyLink'} href={ilmPolicyLink}>
|
||||
<EuiTextColor color="subdued">{ilmPolicyName}</EuiTextColor>
|
||||
</EuiLink>
|
||||
) : (
|
||||
ilmPolicyName
|
||||
)}
|
||||
</>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
ilmPolicyName
|
||||
<>
|
||||
{ilmPolicyLink ? (
|
||||
<EuiLink data-test-subj={'ilmPolicyLink'} href={ilmPolicyLink}>
|
||||
{ilmPolicyName}
|
||||
</EuiLink>
|
||||
) : (
|
||||
ilmPolicyName
|
||||
)}
|
||||
</>
|
||||
),
|
||||
dataTestSubj: 'ilmPolicyDetail',
|
||||
});
|
||||
|
@ -170,6 +213,14 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
return managementDetails;
|
||||
};
|
||||
|
||||
indicesLink = (
|
||||
<EuiLink
|
||||
{...reactRouterNavigate(history, getIndexListUri(`data_stream="${dataStreamName}"`, true))}
|
||||
>
|
||||
{indices.length}
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
const defaultDetails = [
|
||||
{
|
||||
name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.healthTitle', {
|
||||
|
@ -216,16 +267,7 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
toolTip: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.indicesToolTip', {
|
||||
defaultMessage: `The data stream's current backing indices.`,
|
||||
}),
|
||||
content: (
|
||||
<EuiLink
|
||||
{...reactRouterNavigate(
|
||||
history,
|
||||
getIndexListUri(`data_stream="${dataStreamName}"`, true)
|
||||
)}
|
||||
>
|
||||
{indices.length}
|
||||
</EuiLink>
|
||||
),
|
||||
content: indicesLink,
|
||||
dataTestSubj: 'indicesDetail',
|
||||
},
|
||||
{
|
||||
|
@ -271,9 +313,16 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
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: (
|
||||
<ConditionalWrap
|
||||
condition={isDataStreamFullyManagedByILM(dataStream)}
|
||||
wrap={(children) => <EuiTextColor color="subdued">{children}</EuiTextColor>}
|
||||
>
|
||||
<>{getLifecycleValue(lifecycle)}</>
|
||||
</ConditionalWrap>
|
||||
),
|
||||
dataTestSubj: 'dataRetentionDetail',
|
||||
},
|
||||
];
|
||||
|
@ -281,7 +330,43 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
const managementDetails = getManagementDetails();
|
||||
const details = [...defaultDetails, ...managementDetails];
|
||||
|
||||
content = <DetailsList details={details} />;
|
||||
content = (
|
||||
<>
|
||||
{isDataStreamFullyManagedByILM(dataStream) && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.fullyManagedByILMTitle',
|
||||
{ defaultMessage: 'This data stream and its associated indices are managed by ILM' }
|
||||
)}
|
||||
iconType="pin"
|
||||
data-test-subj="dsIsFullyManagedByILM"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.fullyManagedByILMDescription"
|
||||
defaultMessage="To edit data retention for this data stream, you must edit its associated {link}."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href={ilmPolicyLink}>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.fullyManagedByILMButtonLabel"
|
||||
defaultMessage="ILM policy"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DetailsList details={details} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const closePopover = () => {
|
||||
|
@ -310,7 +395,8 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
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<Props> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{isEditingDataRetention && (
|
||||
{isEditingDataRetention && dataStream && (
|
||||
<EditDataRetentionModal
|
||||
onClose={(data) => {
|
||||
if (data && data?.hasUpdatedDataRetention) {
|
||||
|
@ -373,8 +459,9 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
setIsEditingDataRetention(false);
|
||||
}
|
||||
}}
|
||||
dataStreamName={dataStreamName}
|
||||
lifecycle={dataStream?.lifecycle}
|
||||
ilmPolicyName={dataStream?.ilmPolicyName}
|
||||
ilmPolicyLink={ilmPolicyLink}
|
||||
dataStream={dataStream}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { DataStreamDetailPanel } from './data_stream_detail_panel';
|
||||
export { DataStreamDetailPanel, ConditionalWrap } from './data_stream_detail_panel';
|
||||
|
|
|
@ -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<Props> = ({
|
|||
const [dataStreamsToDelete, setDataStreamsToDelete] = useState<string[]>([]);
|
||||
const { config } = useAppContext();
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<DataStream>> = [];
|
||||
const data = useMemo(() => {
|
||||
return (dataStreams || []).map((dataStream) => ({
|
||||
...dataStream,
|
||||
isDataStreamFullyManagedByILM: isDataStreamFullyManagedByILM(dataStream),
|
||||
}));
|
||||
}, [dataStreams]);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<TableDataStream>> = [];
|
||||
|
||||
columns.push({
|
||||
field: 'name',
|
||||
|
@ -137,8 +151,7 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
|||
name: (
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.idxMgmt.dataStreamList.table.dataRetentionColumnTooltip', {
|
||||
defaultMessage:
|
||||
'[Technical preview] Data will be kept at least this long before it is automatically deleted. Only applies to data streams managed by a data stream lifecycle. This value might not apply to all data if the data stream also has an index lifecycle policy.',
|
||||
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.`,
|
||||
})}
|
||||
>
|
||||
<span>
|
||||
|
@ -151,7 +164,14 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
|||
),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (lifecycle: DataStream['lifecycle']) => getLifecycleValue(lifecycle, INFINITE_AS_ICON),
|
||||
render: (lifecycle: DataStream['lifecycle'], dataStream) => (
|
||||
<ConditionalWrap
|
||||
condition={dataStream.isDataStreamFullyManagedByILM}
|
||||
wrap={(children) => <EuiTextColor color="subdued">{children}</EuiTextColor>}
|
||||
>
|
||||
<>{getLifecycleValue(lifecycle, INFINITE_AS_ICON)}</>
|
||||
</ConditionalWrap>
|
||||
),
|
||||
});
|
||||
|
||||
columns.push({
|
||||
|
@ -235,8 +255,8 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
|||
<>
|
||||
{dataStreamsToDelete && dataStreamsToDelete.length > 0 ? (
|
||||
<DeleteDataStreamConfirmationModal
|
||||
onClose={(data) => {
|
||||
if (data && data.hasDeletedDataStreams) {
|
||||
onClose={(res) => {
|
||||
if (res && res.hasDeletedDataStreams) {
|
||||
reload();
|
||||
} else {
|
||||
setDataStreamsToDelete([]);
|
||||
|
@ -246,7 +266,7 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
|||
/>
|
||||
) : null}
|
||||
<EuiInMemoryTable
|
||||
items={dataStreams || []}
|
||||
items={data}
|
||||
itemId="name"
|
||||
columns={columns}
|
||||
search={searchConfig}
|
||||
|
|
|
@ -17,8 +17,9 @@ import {
|
|||
EuiSpacer,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
EuiBadge,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { ScopedHistory } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
|
@ -34,15 +35,19 @@ import {
|
|||
NumericField,
|
||||
} from '../../../../../shared_imports';
|
||||
|
||||
import { reactRouterNavigate } from '../../../../../shared_imports';
|
||||
import { getIndexListUri } from '../../../../services/routing';
|
||||
import { documentationService } from '../../../../services/documentation';
|
||||
import { splitSizeAndUnits, DataStream } from '../../../../../../common';
|
||||
import { isDSLWithILMIndices } from '../../../../lib/data_streams';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
import { UnitField } from './unit_field';
|
||||
import { updateDataRetention } from '../../../../services/api';
|
||||
|
||||
interface Props {
|
||||
lifecycle: DataStream['lifecycle'];
|
||||
dataStreamName: string;
|
||||
dataStream: DataStream;
|
||||
ilmPolicyName?: string;
|
||||
ilmPolicyLink: string;
|
||||
onClose: (data?: { hasUpdatedDataRetention: boolean }) => 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',
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
interface MixedIndicesCalloutProps {
|
||||
history: ScopedHistory;
|
||||
ilmPolicyLink: string;
|
||||
ilmPolicyName?: string;
|
||||
dataStreamName: string;
|
||||
}
|
||||
|
||||
const MixedIndicesCallout = ({
|
||||
ilmPolicyLink,
|
||||
ilmPolicyName,
|
||||
dataStreamName,
|
||||
history,
|
||||
}: MixedIndicesCalloutProps) => {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.someManagedByILMTitle',
|
||||
{ defaultMessage: 'Some indices are managed by ILM' }
|
||||
)}
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
data-test-subj="someIndicesAreManagedByILMCallout"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.someManagedByILMBody"
|
||||
defaultMessage="One or more indices are managed by an ILM policy ({viewAllIndicesLink}). Updating data retention for this data stream won't affect these indices. Instead you will have to update the {ilmPolicyLink} policy."
|
||||
values={{
|
||||
ilmPolicyLink: (
|
||||
<EuiLink data-test-subj="viewIlmPolicyLink" href={ilmPolicyLink}>
|
||||
{ilmPolicyName}
|
||||
</EuiLink>
|
||||
),
|
||||
viewAllIndicesLink: (
|
||||
<EuiLink
|
||||
{...reactRouterNavigate(
|
||||
history,
|
||||
getIndexListUri(`data_stream="${dataStreamName}"`, true)
|
||||
)}
|
||||
data-test-subj="viewAllIndicesLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.viewAllIndices"
|
||||
defaultMessage="view indices"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditDataRetentionModal: React.FunctionComponent<Props> = ({
|
||||
lifecycle,
|
||||
dataStreamName,
|
||||
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<Props> = ({
|
|||
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<Props> = ({
|
|||
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<Props> = ({
|
|||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.modalTitleText"
|
||||
defaultMessage="Edit data retention"
|
||||
/>{' '}
|
||||
<EuiBadge color="hollow">
|
||||
<EuiText size="xs">
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.techPreviewLabel',
|
||||
{
|
||||
defaultMessage: 'Technical preview',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiBadge>
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
{dslWithIlmIndices && (
|
||||
<>
|
||||
<MixedIndicesCallout
|
||||
history={history}
|
||||
ilmPolicyLink={ilmPolicyLink}
|
||||
ilmPolicyName={ilmPolicyName}
|
||||
dataStreamName={dataStreamName}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
|
||||
<UseField
|
||||
path="dataRetentionEnabled"
|
||||
component={ToggleField}
|
||||
data-test-subj="dataRetentionEnabledField"
|
||||
/>
|
||||
|
||||
<UseField
|
||||
path="dataRetention"
|
||||
component={NumericField}
|
||||
|
@ -247,14 +335,14 @@ export const EditDataRetentionModal: React.FunctionComponent<Props> = ({
|
|||
componentProps={{
|
||||
fullWidth: false,
|
||||
euiFieldProps: {
|
||||
disabled: formData.infiniteRetentionPeriod,
|
||||
disabled: formData.infiniteRetentionPeriod || !formData.dataRetentionEnabled,
|
||||
'data-test-subj': `dataRetentionValue`,
|
||||
min: 1,
|
||||
append: (
|
||||
<UnitField
|
||||
path="timeUnit"
|
||||
options={timeUnits}
|
||||
disabled={formData.infiniteRetentionPeriod}
|
||||
disabled={formData.infiniteRetentionPeriod || !formData.dataRetentionEnabled}
|
||||
euiFieldProps={{
|
||||
'data-test-subj': 'timeUnit',
|
||||
'aria-label': i18n.translate(
|
||||
|
@ -274,6 +362,11 @@ export const EditDataRetentionModal: React.FunctionComponent<Props> = ({
|
|||
path="infiniteRetentionPeriod"
|
||||
component={ToggleField}
|
||||
data-test-subj="infiniteRetentionPeriod"
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
disabled: !formData.dataRetentionEnabled,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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<typeof paramsSchema>;
|
||||
const { dataRetention } = request.body as TypeOf<typeof bodySchema>;
|
||||
const { dataRetention, enabled } = request.body as TypeOf<typeof bodySchema>;
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue