[8.11] [Index Management] Fix small edit lifecycle bugs (#168560) (#168967)

# Backport

This will backport the following commits from `main` to `8.11`:
- [[Index Management] Fix small edit lifecycle bugs
(#168560)](https://github.com/elastic/kibana/pull/168560)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Ignacio
Rivas","email":"rivasign@gmail.com"},"sourceCommit":{"committedDate":"2023-10-16T09:51:50Z","message":"[Index
Management] Fix small edit lifecycle bugs
(#168560)","sha":"3488c83dc5abef2741cabfce277202a7f24ff951","branchLabelMapping":{"^v8.12.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Index
Management","Team:Deployment
Management","release_note:skip","backport:skip","v8.11.0","v8.12.0"],"number":168560,"url":"https://github.com/elastic/kibana/pull/168560","mergeCommit":{"message":"[Index
Management] Fix small edit lifecycle bugs
(#168560)","sha":"3488c83dc5abef2741cabfce277202a7f24ff951"}},"sourceBranch":"main","suggestedTargetBranches":["8.11"],"targetPullRequestStates":[{"branch":"8.11","label":"v8.11.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.12.0","labelRegex":"^v8.12.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/168560","number":168560,"mergeCommit":{"message":"[Index
Management] Fix small edit lifecycle bugs
(#168560)","sha":"3488c83dc5abef2741cabfce277202a7f24ff951"}}]}]
BACKPORT-->
This commit is contained in:
Ignacio Rivas 2023-10-16 17:42:51 +02:00 committed by GitHub
parent 9659b2bdec
commit 70b88bed7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 194 additions and 60 deletions

View file

@ -102,4 +102,5 @@ export type TestSubjects =
| 'filter-option-h'
| 'infiniteRetentionPeriod.input'
| 'saveButton'
| 'dataRetentionDetail'
| 'createIndexSaveButton';

View file

@ -278,6 +278,7 @@ export const createDataStreamPayload = (dataStream: Partial<DataStream>): DataSt
},
hidden: false,
lifecycle: {
enabled: true,
data_retention: '7d',
},
...dataStream,

View file

@ -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 },

View file

@ -59,7 +59,9 @@ export interface DataStream {
_meta?: Metadata;
privileges: Privileges;
hidden: boolean;
lifecycle?: IndicesDataLifecycleWithRollover;
lifecycle?: IndicesDataLifecycleWithRollover & {
enabled?: boolean;
};
}
export interface DataStreamIndex {

View file

@ -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');
});
});
});

View file

@ -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 (
<EuiToolTip
data-test-subj="infiniteRetention"
position="top"
content={infiniteDataRetention}
>
<EuiIcon type="infinity" />
</EuiToolTip>
);
}
return infiniteDataRetention;
}
return lifecycle?.data_retention;
};

View file

@ -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<Props> = ({
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<Props> = ({
),
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<Props> = ({
}
}}
dataStreamName={dataStreamName}
dataRetention={dataStream?.lifecycle?.data_retention as string}
lifecycle={dataStream?.lifecycle}
/>
)}

View file

@ -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<Props> = ({
dataStreams,
reload,
@ -144,7 +147,7 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
),
truncateText: true,
sortable: true,
render: (lifecycle: DataStream['lifecycle']) => lifecycle?.data_retention,
render: (lifecycle: DataStream['lifecycle']) => getLifecycleValue(lifecycle, INFINITE_AS_ICON),
});
columns.push({

View file

@ -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<Props> = ({
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<Props> = ({
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',

View file

@ -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';

View file

@ -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;