mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Index management] Data stream edit data retention (#167006)
This commit is contained in:
parent
33183c2d01
commit
d35fa69138
30 changed files with 1121 additions and 39 deletions
|
@ -413,6 +413,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
secureCluster: `${ELASTICSEARCH_DOCS}secure-cluster.html`,
|
||||
shardAllocationSettings: `${ELASTICSEARCH_DOCS}modules-cluster.html#cluster-shard-allocation-settings`,
|
||||
sortSearch: `${ELASTICSEARCH_DOCS}sort-search-results.html`,
|
||||
tutorialUpdateExistingDataStream: `${ELASTICSEARCH_DOCS}tutorial-manage-existing-data-stream.html`,
|
||||
transportSettings: `${ELASTICSEARCH_DOCS}modules-network.html#common-network-settings`,
|
||||
typesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`,
|
||||
setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`,
|
||||
|
|
|
@ -90,6 +90,18 @@ const registerHttpRequestMockHelpers = (
|
|||
const setDeleteDataStreamResponse = (response?: HttpResponse, error?: ResponseError) =>
|
||||
mockResponse('POST', `${API_BASE_PATH}/delete_data_streams`, response, error);
|
||||
|
||||
const setEditDataRetentionResponse = (
|
||||
dataStreamId: string,
|
||||
response?: HttpResponse,
|
||||
error?: ResponseError
|
||||
) =>
|
||||
mockResponse(
|
||||
'PUT',
|
||||
`${API_BASE_PATH}/data_streams/${encodeURIComponent(dataStreamId)}/data_retention`,
|
||||
response,
|
||||
error
|
||||
);
|
||||
|
||||
const setDeleteTemplateResponse = (response?: HttpResponse, error?: ResponseError) =>
|
||||
mockResponse('POST', `${API_BASE_PATH}/delete_index_templates`, response, error);
|
||||
|
||||
|
@ -196,6 +208,7 @@ const registerHttpRequestMockHelpers = (
|
|||
setLoadDataStreamResponse,
|
||||
setDeleteDataStreamResponse,
|
||||
setDeleteTemplateResponse,
|
||||
setEditDataRetentionResponse,
|
||||
setLoadTemplateResponse,
|
||||
setCreateTemplateResponse,
|
||||
setLoadIndexSettingsResponse,
|
||||
|
|
|
@ -92,5 +92,14 @@ export type TestSubjects =
|
|||
| 'createAndExecuteButton'
|
||||
| 'enrichPolicySummaryList'
|
||||
| 'requestBody'
|
||||
| 'editDataRetentionButton'
|
||||
| 'errorWhenCreatingCallout'
|
||||
| 'manageDataStreamButton'
|
||||
| 'dataRetentionValue'
|
||||
| 'policyNameField'
|
||||
| 'configuredByILMWarning'
|
||||
| 'show-filters-button'
|
||||
| 'filter-option-h'
|
||||
| 'infiniteRetentionPeriod.input'
|
||||
| 'saveButton'
|
||||
| 'createIndexSaveButton';
|
||||
|
|
|
@ -34,6 +34,7 @@ export interface DataStreamsTabTestBed extends TestBed<TestSubjects> {
|
|||
selectDataStream: (name: string, selected: boolean) => void;
|
||||
clickConfirmDelete: () => void;
|
||||
clickDeleteDataStreamButton: () => void;
|
||||
clickEditDataRetentionButton: () => void;
|
||||
clickDetailPanelIndexTemplateLink: () => void;
|
||||
};
|
||||
findDeleteActionAt: (index: number) => ReactWrapper;
|
||||
|
@ -176,8 +177,13 @@ export const setup = async (
|
|||
};
|
||||
|
||||
const clickDeleteDataStreamButton = () => {
|
||||
const { find } = testBed;
|
||||
find('deleteDataStreamButton').simulate('click');
|
||||
testBed.find('manageDataStreamButton').simulate('click');
|
||||
testBed.find('deleteDataStreamButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickEditDataRetentionButton = () => {
|
||||
testBed.find('manageDataStreamButton').simulate('click');
|
||||
testBed.find('editDataRetentionButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickDetailPanelIndexTemplateLink = async () => {
|
||||
|
@ -236,6 +242,7 @@ export const setup = async (
|
|||
selectDataStream,
|
||||
clickConfirmDelete,
|
||||
clickDeleteDataStreamButton,
|
||||
clickEditDataRetentionButton,
|
||||
clickDetailPanelIndexTemplateLink,
|
||||
},
|
||||
findDeleteActionAt,
|
||||
|
@ -267,6 +274,7 @@ export const createDataStreamPayload = (dataStream: Partial<DataStream>): DataSt
|
|||
maxTimeStamp: 420,
|
||||
privileges: {
|
||||
delete_index: true,
|
||||
manage_data_stream_lifecycle: true,
|
||||
},
|
||||
hidden: false,
|
||||
lifecycle: {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { notificationServiceMock } from '@kbn/core/public/mocks';
|
||||
|
||||
import {
|
||||
breadcrumbService,
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
import { API_BASE_PATH } from '../../../common/constants';
|
||||
import * as fixtures from '../../../test/fixtures';
|
||||
import { setupEnvironment } from '../helpers';
|
||||
import { notificationService } from '../../../public/application/services/notification';
|
||||
|
||||
import {
|
||||
DataStreamsTabTestBed,
|
||||
|
@ -134,6 +136,8 @@ describe('Data Streams tab', () => {
|
|||
});
|
||||
|
||||
describe('when there are data streams', () => {
|
||||
const notificationsServiceMock = notificationServiceMock.createStartContract();
|
||||
|
||||
beforeEach(async () => {
|
||||
const {
|
||||
setLoadIndicesResponse,
|
||||
|
@ -169,7 +173,13 @@ describe('Data Streams tab', () => {
|
|||
setLoadTemplatesResponse({ templates: [indexTemplate], legacyTemplates: [] });
|
||||
setLoadTemplateResponse(indexTemplate.name, indexTemplate);
|
||||
|
||||
testBed = await setup(httpSetup, { history: createMemoryHistory() });
|
||||
notificationService.setup(notificationsServiceMock);
|
||||
testBed = await setup(httpSetup, {
|
||||
history: createMemoryHistory(),
|
||||
services: {
|
||||
notificationService,
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
testBed.actions.goToDataStreamsList();
|
||||
});
|
||||
|
@ -327,6 +337,64 @@ describe('Data Streams tab', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('update data retention', () => {
|
||||
test('can set data retention period', async () => {
|
||||
const {
|
||||
actions: { clickNameAt, clickEditDataRetentionButton },
|
||||
} = testBed;
|
||||
|
||||
await clickNameAt(0);
|
||||
|
||||
clickEditDataRetentionButton();
|
||||
|
||||
httpRequestsMockHelpers.setEditDataRetentionResponse('dataStream1', {
|
||||
success: true,
|
||||
});
|
||||
|
||||
// set data retention value
|
||||
testBed.form.setInputValue('dataRetentionValue', '7');
|
||||
// Set data retention unit
|
||||
testBed.find('show-filters-button').simulate('click');
|
||||
testBed.find('filter-option-h').simulate('click');
|
||||
|
||||
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({ dataRetention: '7h' }) })
|
||||
);
|
||||
});
|
||||
|
||||
test('allows to set infinite retention period', async () => {
|
||||
const {
|
||||
actions: { clickNameAt, clickEditDataRetentionButton },
|
||||
} = testBed;
|
||||
|
||||
await clickNameAt(0);
|
||||
|
||||
clickEditDataRetentionButton();
|
||||
|
||||
httpRequestsMockHelpers.setEditDataRetentionResponse('dataStream1', {
|
||||
success: true,
|
||||
});
|
||||
|
||||
testBed.form.toggleEuiSwitch('infiniteRetentionPeriod.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({}) })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('clicking index template name navigates to the index template details', async () => {
|
||||
const {
|
||||
actions: { clickNameAt, clickDetailPanelIndexTemplateLink },
|
||||
|
@ -423,6 +491,33 @@ 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;
|
||||
|
||||
|
@ -567,15 +662,29 @@ describe('Data Streams tab', () => {
|
|||
|
||||
const dataStreamWithDelete = createDataStreamPayload({
|
||||
name: 'dataStreamWithDelete',
|
||||
privileges: { delete_index: true },
|
||||
privileges: { delete_index: true, manage_data_stream_lifecycle: true },
|
||||
});
|
||||
const dataStreamNoDelete = createDataStreamPayload({
|
||||
name: 'dataStreamNoDelete',
|
||||
privileges: { delete_index: false },
|
||||
privileges: { delete_index: false, manage_data_stream_lifecycle: true },
|
||||
});
|
||||
const dataStreamNoEditRetention = createDataStreamPayload({
|
||||
name: 'dataStreamNoEditRetention',
|
||||
privileges: { delete_index: true, manage_data_stream_lifecycle: false },
|
||||
});
|
||||
|
||||
const dataStreamNoPermissions = createDataStreamPayload({
|
||||
name: 'dataStreamNoPermissions',
|
||||
privileges: { delete_index: false, manage_data_stream_lifecycle: false },
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
setLoadDataStreamsResponse([dataStreamWithDelete, dataStreamNoDelete]);
|
||||
setLoadDataStreamsResponse([
|
||||
dataStreamWithDelete,
|
||||
dataStreamNoDelete,
|
||||
dataStreamNoEditRetention,
|
||||
dataStreamNoPermissions,
|
||||
]);
|
||||
|
||||
testBed = await setup(httpSetup, { history: createMemoryHistory(), url: urlServiceMock });
|
||||
await act(async () => {
|
||||
|
@ -590,6 +699,8 @@ describe('Data Streams tab', () => {
|
|||
|
||||
expect(tableCellsValues).toEqual([
|
||||
['', 'dataStreamNoDelete', 'green', '1', '7d', ''],
|
||||
['', 'dataStreamNoEditRetention', 'green', '1', '7d', 'Delete'],
|
||||
['', 'dataStreamNoPermissions', 'green', '1', '7d', ''],
|
||||
['', 'dataStreamWithDelete', 'green', '1', '7d', 'Delete'],
|
||||
]);
|
||||
});
|
||||
|
@ -610,17 +721,6 @@ describe('Data Streams tab', () => {
|
|||
expect(find('deleteDataStreamsButton').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('displays delete button in detail panel', async () => {
|
||||
const {
|
||||
actions: { clickNameAt },
|
||||
find,
|
||||
} = testBed;
|
||||
setLoadDataStreamResponse(dataStreamWithDelete.name, dataStreamWithDelete);
|
||||
await clickNameAt(1);
|
||||
|
||||
expect(find('deleteDataStreamButton').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('hides delete button in detail panel', async () => {
|
||||
const {
|
||||
actions: { clickNameAt },
|
||||
|
@ -629,8 +729,44 @@ describe('Data Streams tab', () => {
|
|||
setLoadDataStreamResponse(dataStreamNoDelete.name, dataStreamNoDelete);
|
||||
await clickNameAt(0);
|
||||
|
||||
testBed.find('manageDataStreamButton').simulate('click');
|
||||
expect(find('deleteDataStreamButton').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('hides edit data retention button if no permissions', async () => {
|
||||
const {
|
||||
actions: { clickNameAt },
|
||||
find,
|
||||
} = testBed;
|
||||
setLoadDataStreamResponse(dataStreamNoEditRetention.name, dataStreamNoEditRetention);
|
||||
await clickNameAt(1);
|
||||
|
||||
testBed.find('manageDataStreamButton').simulate('click');
|
||||
expect(find('editDataRetentionButton').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('hides manage button if no permissions', async () => {
|
||||
const {
|
||||
actions: { clickNameAt },
|
||||
find,
|
||||
} = testBed;
|
||||
setLoadDataStreamResponse(dataStreamNoPermissions.name, dataStreamNoPermissions);
|
||||
await clickNameAt(2);
|
||||
|
||||
expect(find('manageDataStreamButton').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('displays delete button in detail panel', async () => {
|
||||
const {
|
||||
actions: { clickNameAt },
|
||||
find,
|
||||
} = testBed;
|
||||
setLoadDataStreamResponse(dataStreamWithDelete.name, dataStreamWithDelete);
|
||||
await clickNameAt(3);
|
||||
|
||||
testBed.find('manageDataStreamButton').simulate('click');
|
||||
expect(find('deleteDataStreamButton').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,6 @@
|
|||
|
||||
export { API_BASE_PATH, INTERNAL_API_BASE_PATH, BASE_PATH, MAJOR_VERSION } from './constants';
|
||||
|
||||
export { getTemplateParameter } from './lib';
|
||||
export { getTemplateParameter, splitSizeAndUnits } from './lib';
|
||||
|
||||
export * from './types';
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { splitSizeAndUnits } from './data_stream_serialization';
|
||||
|
||||
describe('Data stream serialization', () => {
|
||||
test('can split size and units from lifecycle string', () => {
|
||||
expect(splitSizeAndUnits('1h')).toEqual({ size: '1', unit: 'h' });
|
||||
expect(splitSizeAndUnits('20micron')).toEqual({ size: '20', unit: 'micron' });
|
||||
});
|
||||
});
|
|
@ -54,3 +54,19 @@ export function deserializeDataStreamList(
|
|||
): DataStream[] {
|
||||
return dataStreamsFromEs.map((dataStream) => deserializeDataStream(dataStream));
|
||||
}
|
||||
|
||||
export const splitSizeAndUnits = (field: string): { size: string; unit: string } => {
|
||||
let size = '';
|
||||
let unit = '';
|
||||
|
||||
const result = /(\d+)(\w+)/.exec(field);
|
||||
if (result) {
|
||||
size = result[1];
|
||||
unit = result[2];
|
||||
}
|
||||
|
||||
return {
|
||||
size,
|
||||
unit,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { deserializeDataStream, deserializeDataStreamList } from './data_stream_serialization';
|
||||
export {
|
||||
deserializeDataStream,
|
||||
deserializeDataStreamList,
|
||||
splitSizeAndUnits,
|
||||
} from './data_stream_serialization';
|
||||
|
||||
export {
|
||||
deserializeTemplate,
|
||||
|
|
|
@ -21,6 +21,7 @@ type TimestampField = TimestampFieldFromEs;
|
|||
|
||||
interface PrivilegesFromEs {
|
||||
delete_index: boolean;
|
||||
manage_data_stream_lifecycle: boolean;
|
||||
}
|
||||
|
||||
type Privileges = PrivilegesFromEs;
|
||||
|
@ -33,6 +34,7 @@ export interface EnhancedDataStreamFromEs extends IndicesDataStream {
|
|||
maximum_timestamp?: IndicesDataStreamsStatsDataStreamsStatsItem['maximum_timestamp'];
|
||||
privileges: {
|
||||
delete_index: boolean;
|
||||
manage_data_stream_lifecycle: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
|
@ -22,6 +23,10 @@ import {
|
|||
EuiIconTip,
|
||||
EuiLink,
|
||||
EuiTitle,
|
||||
EuiIcon,
|
||||
EuiPopover,
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanelDescriptor,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { DiscoverLink } from '../../../../lib/discover_link';
|
||||
|
@ -29,6 +34,7 @@ import { SectionLoading, reactRouterNavigate } from '../../../../../shared_impor
|
|||
import { SectionError, Error, DataHealth } from '../../../../components';
|
||||
import { useLoadDataStream } from '../../../../services/api';
|
||||
import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
|
||||
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';
|
||||
|
@ -70,7 +76,10 @@ const DetailsList: React.FunctionComponent<DetailsListProps> = ({ details }) =>
|
|||
|
||||
const midpoint = Math.ceil(descriptionListItems.length / 2);
|
||||
const descriptionListColumnOne = descriptionListItems.slice(0, midpoint);
|
||||
const descriptionListColumnTwo = descriptionListItems.slice(-midpoint);
|
||||
const descriptionListColumnTwo = descriptionListItems.slice(
|
||||
midpoint,
|
||||
descriptionListItems.length
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
|
@ -93,9 +102,11 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
dataStreamName,
|
||||
onClose,
|
||||
}) => {
|
||||
const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName);
|
||||
|
||||
const [isManagePopOverOpen, setManagePopOver] = useState<boolean>(false);
|
||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||
const [isEditingDataRetention, setIsEditingDataRetention] = useState<boolean>(false);
|
||||
|
||||
const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName);
|
||||
|
||||
const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, dataStream?.ilmPolicyName);
|
||||
const { history } = useAppContext();
|
||||
|
@ -275,9 +286,74 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
content = <DetailsList details={details} />;
|
||||
}
|
||||
|
||||
const closePopover = () => {
|
||||
setManagePopOver(false);
|
||||
};
|
||||
|
||||
const button = (
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
data-test-subj="manageDataStreamButton"
|
||||
onClick={() => setManagePopOver(!isManagePopOverOpen)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamsDetailsPanel.manageButtonLabel"
|
||||
defaultMessage="Manage"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
const panels: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
id: 0,
|
||||
title: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.managePanelTitle', {
|
||||
defaultMessage: 'Data stream options',
|
||||
}),
|
||||
items: [
|
||||
...(!dataStream?.ilmPolicyName && dataStream?.privileges?.manage_data_stream_lifecycle
|
||||
? [
|
||||
{
|
||||
key: 'editDataRetention',
|
||||
name: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamDetailPanel.managePanelEditDataRetention',
|
||||
{
|
||||
defaultMessage: 'Edit data retention',
|
||||
}
|
||||
),
|
||||
'data-test-subj': 'editDataRetentionButton',
|
||||
icon: <EuiIcon type="pencil" size="m" />,
|
||||
onClick: () => {
|
||||
closePopover();
|
||||
setIsEditingDataRetention(true);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(dataStream?.privileges?.delete_index
|
||||
? [
|
||||
{
|
||||
key: 'deleteDataStream',
|
||||
name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.managePanelDelete', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
'data-test-subj': 'deleteDataStreamButton',
|
||||
icon: <EuiIcon type="trash" size="m" color="danger" />,
|
||||
onClick: () => {
|
||||
closePopover();
|
||||
setIsDeleting(true);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDeleting ? (
|
||||
{isDeleting && (
|
||||
<DeleteDataStreamConfirmationModal
|
||||
onClose={(data) => {
|
||||
if (data && data.hasDeletedDataStreams) {
|
||||
|
@ -288,7 +364,21 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
}}
|
||||
dataStreams={[dataStreamName]}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
{isEditingDataRetention && (
|
||||
<EditDataRetentionModal
|
||||
onClose={(data) => {
|
||||
if (data && data?.hasUpdatedDataRetention) {
|
||||
onClose(true);
|
||||
} else {
|
||||
setIsEditingDataRetention(false);
|
||||
}
|
||||
}}
|
||||
dataStreamName={dataStreamName}
|
||||
dataRetention={dataStream?.lifecycle?.data_retention as string}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EuiFlyout
|
||||
onClose={() => onClose()}
|
||||
|
@ -324,20 +414,19 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
{!isLoading && !error && dataStream?.privileges.delete_index ? (
|
||||
{!isLoading && !error && panels[0].items?.length && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={() => setIsDeleting(true)}
|
||||
data-test-subj="deleteDataStreamButton"
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isManagePopOverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.deleteButtonLabel', {
|
||||
defaultMessage: 'Delete data stream',
|
||||
})}
|
||||
</EuiButton>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
useForm,
|
||||
useFormData,
|
||||
useFormIsModified,
|
||||
Form,
|
||||
fieldFormatters,
|
||||
FormSchema,
|
||||
FIELD_TYPES,
|
||||
UseField,
|
||||
ToggleField,
|
||||
NumericField,
|
||||
} from '../../../../../shared_imports';
|
||||
|
||||
import { documentationService } from '../../../../services/documentation';
|
||||
import { splitSizeAndUnits } from '../../../../../../common';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
import { UnitField } from './unit_field';
|
||||
import { updateDataRetention } from '../../../../services/api';
|
||||
|
||||
interface Props {
|
||||
dataRetention: string;
|
||||
dataStreamName: string;
|
||||
onClose: (data?: { hasUpdatedDataRetention: boolean }) => void;
|
||||
}
|
||||
|
||||
export const timeUnits = [
|
||||
{
|
||||
value: 'd',
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.timeUnits.daysLabel',
|
||||
{
|
||||
defaultMessage: 'days',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'h',
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.timeUnits.hoursLabel',
|
||||
{
|
||||
defaultMessage: 'hours',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'm',
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.timeUnits.minutesLabel',
|
||||
{
|
||||
defaultMessage: 'minutes',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 's',
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.timeUnits.secondsLabel',
|
||||
{
|
||||
defaultMessage: 'seconds',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
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 = {
|
||||
dataRetention: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.dataRetentionField',
|
||||
{
|
||||
defaultMessage: 'Data retention',
|
||||
}
|
||||
),
|
||||
formatters: [fieldFormatters.toInt],
|
||||
validations: [
|
||||
{
|
||||
validator: ({ value }) => {
|
||||
if (!value) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.dataRetentionFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'A data retention value is required.',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
if (value <= 0) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.dataRetentionFieldNonNegativeError',
|
||||
{
|
||||
defaultMessage: `A positive value is required.`,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
timeUnit: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.timeUnitField',
|
||||
{
|
||||
defaultMessage: 'Time unit',
|
||||
}
|
||||
),
|
||||
},
|
||||
infiniteRetentionPeriod: {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
defaultValue: false,
|
||||
label: i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.infiniteRetentionPeriodField',
|
||||
{
|
||||
defaultMessage: 'Keep data indefinitely',
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const EditDataRetentionModal: React.FunctionComponent<Props> = ({
|
||||
dataRetention,
|
||||
dataStreamName,
|
||||
onClose,
|
||||
}) => {
|
||||
const { size, unit } = splitSizeAndUnits(dataRetention);
|
||||
const {
|
||||
services: { notificationService },
|
||||
} = useAppContext();
|
||||
|
||||
const { form } = useForm({
|
||||
defaultValue: {
|
||||
dataRetention: size,
|
||||
timeUnit: unit || 'd',
|
||||
infiniteRetentionPeriod: !dataRetention,
|
||||
},
|
||||
schema: configurationFormSchema,
|
||||
id: 'editDataRetentionForm',
|
||||
});
|
||||
const [formData] = useFormData({ form });
|
||||
const isDirty = useFormIsModified({ form });
|
||||
|
||||
const onSubmitForm = async () => {
|
||||
const { isValid, data } = await form.submit();
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
return updateDataRetention(dataStreamName, data).then(({ data: responseData, error }) => {
|
||||
if (responseData) {
|
||||
const successMessage = i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.successDataRetentionNotification',
|
||||
{ defaultMessage: 'Data retention updated' }
|
||||
);
|
||||
notificationService.showSuccessToast(successMessage);
|
||||
|
||||
return onClose({ hasUpdatedDataRetention: true });
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorMessage = i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.errorDataRetentionNotification',
|
||||
{
|
||||
defaultMessage: "Error updating data retention: '{error}'",
|
||||
values: { error: error.message },
|
||||
}
|
||||
);
|
||||
notificationService.showDangerToast(errorMessage);
|
||||
}
|
||||
|
||||
onClose();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiModal onClose={() => onClose()} data-test-subj="editDataRetentionModal">
|
||||
<Form form={form} data-test-subj="editDataRetentionForm">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.modalTitleText"
|
||||
defaultMessage="Edit data retention"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<UseField
|
||||
path="dataRetention"
|
||||
component={NumericField}
|
||||
labelAppend={
|
||||
<EuiText size="xs">
|
||||
<EuiLink href={documentationService.getUpdateExistingDS()} target="_blank" external>
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.learnMoreLinkText',
|
||||
{
|
||||
defaultMessage: 'How does it work?',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
}
|
||||
componentProps={{
|
||||
fullWidth: false,
|
||||
euiFieldProps: {
|
||||
disabled: formData.infiniteRetentionPeriod,
|
||||
'data-test-subj': `dataRetentionValue`,
|
||||
min: 1,
|
||||
append: (
|
||||
<UnitField
|
||||
path="timeUnit"
|
||||
options={timeUnits}
|
||||
disabled={formData.infiniteRetentionPeriod}
|
||||
euiFieldProps={{
|
||||
'data-test-subj': 'timeUnit',
|
||||
'aria-label': i18n.translate(
|
||||
'xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.unitsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Time unit',
|
||||
}
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<UseField
|
||||
path="infiniteRetentionPeriod"
|
||||
component={ToggleField}
|
||||
data-test-subj="infiniteRetentionPeriod"
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty data-test-subj="cancelButton" onClick={() => onClose()}>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
fill
|
||||
type="submit"
|
||||
isLoading={false}
|
||||
disabled={(form.isSubmitted && form.isValid === false) || !isDirty}
|
||||
data-test-subj="saveButton"
|
||||
onClick={onSubmitForm}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamsDetailsPanel.editDataRetentionModal.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</Form>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { EditDataRetentionModal } from './edit_data_retention_modal';
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 React, { FunctionComponent, useState } from 'react';
|
||||
import { EuiFilterSelectItem, EuiPopover, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { UseField } from '../../../../../shared_imports';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
disabled?: boolean;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
options: Array<{
|
||||
value: string;
|
||||
text: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const UnitField: FunctionComponent<Props> = ({ path, disabled, options, euiFieldProps }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<UseField key={path} path={path}>
|
||||
{(field) => {
|
||||
const onSelect = (option: string) => {
|
||||
field.setValue(option);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="text"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
onClick={() => setOpen((isOpen) => !isOpen)}
|
||||
data-test-subj="show-filters-button"
|
||||
disabled={disabled}
|
||||
>
|
||||
{options.find((timeUnitOption) => timeUnitOption.value === field.value)?.text ??
|
||||
`${field.value}`}
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
ownFocus
|
||||
panelPaddingSize="none"
|
||||
isOpen={open}
|
||||
closePopover={() => setOpen(false)}
|
||||
{...euiFieldProps}
|
||||
>
|
||||
{options.map((item) => (
|
||||
<EuiFilterSelectItem
|
||||
key={item.value}
|
||||
checked={field.value === item.value ? 'on' : undefined}
|
||||
onClick={() => onSelect(item.value)}
|
||||
data-test-subj={`filter-option-${item.value}`}
|
||||
>
|
||||
{item.text}
|
||||
</EuiFilterSelectItem>
|
||||
))}
|
||||
</EuiPopover>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
);
|
||||
};
|
|
@ -84,6 +84,19 @@ export async function deleteDataStreams(dataStreams: string[]) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function updateDataRetention(
|
||||
name: string,
|
||||
data: { dataRetention: string; timeUnit: string; infiniteRetentionPeriod: boolean }
|
||||
) {
|
||||
return sendRequest({
|
||||
path: `${API_BASE_PATH}/data_streams/${encodeURIComponent(name)}/data_retention`,
|
||||
method: 'put',
|
||||
body: data.infiniteRetentionPeriod
|
||||
? {}
|
||||
: { dataRetention: `${data.dataRetention}${data.timeUnit}` },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadIndices() {
|
||||
const response = await httpService.httpClient.get<any>(`${API_BASE_PATH}/indices`);
|
||||
return response.data ? response.data : response;
|
||||
|
|
|
@ -64,6 +64,7 @@ class DocumentationService {
|
|||
private bulkIndexAlias: string = '';
|
||||
private indexStats: string = '';
|
||||
private bulkApi: string = '';
|
||||
private updateExistingDS: string = '';
|
||||
|
||||
public setup(docLinks: DocLinksStart): void {
|
||||
const { links } = docLinks;
|
||||
|
@ -121,6 +122,7 @@ class DocumentationService {
|
|||
this.bulkIndexAlias = links.apis.bulkIndexAlias;
|
||||
this.indexStats = links.apis.indexStats;
|
||||
this.bulkApi = links.enterpriseSearch.bulkApi;
|
||||
this.updateExistingDS = links.elasticsearch.tutorialUpdateExistingDataStream;
|
||||
}
|
||||
|
||||
public getEsDocsBase() {
|
||||
|
@ -341,6 +343,10 @@ class DocumentationService {
|
|||
return this.bulkApi;
|
||||
}
|
||||
|
||||
public getUpdateExistingDS() {
|
||||
return this.updateExistingDS;
|
||||
}
|
||||
|
||||
public getWellKnownTextLink() {
|
||||
return 'http://docs.opengeospatial.org/is/12-063r5/12-063r5.html';
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ export {
|
|||
VALIDATION_TYPES,
|
||||
useForm,
|
||||
useFormData,
|
||||
useFormIsModified,
|
||||
Form,
|
||||
getUseField,
|
||||
UseField,
|
||||
|
@ -60,6 +61,7 @@ export {
|
|||
TextField,
|
||||
SelectField,
|
||||
ToggleField,
|
||||
NumericField,
|
||||
JsonEditorField,
|
||||
ComboBoxField,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { addBasePath } from '..';
|
||||
import { RouterMock, routeDependencies, RequestMock } from '../../../test/helpers';
|
||||
|
||||
import { registerDataStreamRoutes } from '.';
|
||||
|
||||
describe('Data streams API', () => {
|
||||
const router = new RouterMock();
|
||||
|
||||
beforeEach(() => {
|
||||
registerDataStreamRoutes({
|
||||
...routeDependencies,
|
||||
router,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Update data retention for DS - PUT /internal/index_management/{name}/data_retention', () => {
|
||||
const updateDataLifecycle = router.getMockESApiFn('indices.putDataLifecycle');
|
||||
|
||||
it('updates data lifecycle for a given data stream', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'put',
|
||||
path: addBasePath('/data_streams/{name}/data_retention'),
|
||||
params: { name: 'foo' },
|
||||
body: { dataRetention: '7d' },
|
||||
};
|
||||
|
||||
updateDataLifecycle.mockResolvedValue({ success: true });
|
||||
|
||||
const res = await router.runRequest(mockRequest);
|
||||
|
||||
expect(res).toEqual({
|
||||
body: { success: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if it fails', async () => {
|
||||
const mockRequest: RequestMock = {
|
||||
method: 'put',
|
||||
path: addBasePath('/data_streams/{name}/data_retention'),
|
||||
params: { name: 'foo' },
|
||||
body: { dataRetention: '7d' },
|
||||
};
|
||||
|
||||
const error = new Error('Oh no!');
|
||||
updateDataLifecycle.mockRejectedValue(error);
|
||||
|
||||
await expect(router.runRequest(mockRequest)).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,6 +9,7 @@ import { RouteDependencies } from '../../../types';
|
|||
|
||||
import { registerGetOneRoute, registerGetAllRoute } from './register_get_route';
|
||||
import { registerDeleteRoute } from './register_delete_route';
|
||||
import { registerPutDataRetention } from './register_put_route';
|
||||
import { registerPostOneApplyLatestMappings, registerPostOneRollover } from './register_post_route';
|
||||
|
||||
export function registerDataStreamRoutes(dependencies: RouteDependencies) {
|
||||
|
@ -17,4 +18,5 @@ export function registerDataStreamRoutes(dependencies: RouteDependencies) {
|
|||
registerPostOneRollover(dependencies);
|
||||
registerGetAllRoute(dependencies);
|
||||
registerDeleteRoute(dependencies);
|
||||
registerPutDataRetention(dependencies);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,9 @@ const enhanceDataStreams = ({
|
|||
delete_index: dataStreamsPrivileges
|
||||
? dataStreamsPrivileges.index[dataStream.name].delete_index
|
||||
: true,
|
||||
manage_data_stream_lifecycle: dataStreamsPrivileges
|
||||
? dataStreamsPrivileges.index[dataStream.name].manage_data_stream_lifecycle
|
||||
: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -73,7 +76,7 @@ const getDataStreamsPrivileges = (client: IScopedClusterClient, names: string[])
|
|||
index: [
|
||||
{
|
||||
names,
|
||||
privileges: ['delete_index'],
|
||||
privileges: ['delete_index', 'manage_data_stream_lifecycle'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import { RouteDependencies } from '../../../types';
|
||||
import { addBasePath } from '..';
|
||||
|
||||
export function registerPutDataRetention({ router, lib: { handleEsError } }: RouteDependencies) {
|
||||
const paramsSchema = schema.object({
|
||||
name: schema.string(),
|
||||
});
|
||||
const bodySchema = schema.object({
|
||||
dataRetention: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
router.put(
|
||||
{
|
||||
path: addBasePath('/data_streams/{name}/data_retention'),
|
||||
validate: { params: paramsSchema, body: bodySchema },
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { name } = request.params as TypeOf<typeof paramsSchema>;
|
||||
const { dataRetention } = request.body as TypeOf<typeof bodySchema>;
|
||||
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
|
||||
try {
|
||||
await client.asCurrentUser.indices.putDataLifecycle({
|
||||
name,
|
||||
data_retention: dataRetention,
|
||||
});
|
||||
|
||||
return response.ok({ body: { success: true } });
|
||||
} catch (error) {
|
||||
return handleEsError({ error, response });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -17315,7 +17315,6 @@
|
|||
"xpack.idxMgmt.createTemplate.createLegacyTemplatePageTitle": "Créer un modèle hérité",
|
||||
"xpack.idxMgmt.createTemplate.createTemplatePageTitle": "Créer un modèle",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.closeButtonLabel": "Fermer",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.deleteButtonLabel": "Supprimer le flux de données",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.generationTitle": "Génération",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.generationToolTip": "Nombre cumulatif d'index de sauvegarde créés pour le flux de données",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.healthTitle": "Intégrité",
|
||||
|
|
|
@ -17329,7 +17329,6 @@
|
|||
"xpack.idxMgmt.createTemplate.createLegacyTemplatePageTitle": "レガシーテンプレートの作成",
|
||||
"xpack.idxMgmt.createTemplate.createTemplatePageTitle": "テンプレートを作成",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.closeButtonLabel": "閉じる",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.deleteButtonLabel": "データストリームを削除",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.generationTitle": "生成",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.generationToolTip": "データストリームに作成されたバッキングインデックスの累積数",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.healthTitle": "ヘルス",
|
||||
|
|
|
@ -17329,7 +17329,6 @@
|
|||
"xpack.idxMgmt.createTemplate.createLegacyTemplatePageTitle": "创建旧版模板",
|
||||
"xpack.idxMgmt.createTemplate.createTemplatePageTitle": "创建模板",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.closeButtonLabel": "关闭",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.deleteButtonLabel": "删除数据流",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.generationTitle": "世代",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.generationToolTip": "为数据流创建的后备索引的累积计数",
|
||||
"xpack.idxMgmt.dataStreamDetailPanel.healthTitle": "运行状况",
|
||||
|
|
|
@ -117,6 +117,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
privileges: {
|
||||
delete_index: true,
|
||||
manage_data_stream_lifecycle: true,
|
||||
},
|
||||
timeStampField: { name: '@timestamp' },
|
||||
indices: [
|
||||
|
@ -156,6 +157,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
name: testDataStreamName,
|
||||
privileges: {
|
||||
delete_index: true,
|
||||
manage_data_stream_lifecycle: true,
|
||||
},
|
||||
timeStampField: { name: '@timestamp' },
|
||||
indices: [
|
||||
|
@ -190,6 +192,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
name: testDataStreamName,
|
||||
privileges: {
|
||||
delete_index: true,
|
||||
manage_data_stream_lifecycle: true,
|
||||
},
|
||||
timeStampField: { name: '@timestamp' },
|
||||
indices: [
|
||||
|
@ -210,6 +213,35 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Update', () => {
|
||||
const testDataStreamName = 'test-data-stream';
|
||||
|
||||
before(async () => await createDataStream(testDataStreamName));
|
||||
after(async () => await deleteDataStream(testDataStreamName));
|
||||
|
||||
it('updates the data retention of a DS', async () => {
|
||||
const { body } = await supertest
|
||||
.put(`${API_BASE_PATH}/data_streams/${testDataStreamName}/data_retention`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({
|
||||
dataRetention: '7d',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({ success: true });
|
||||
});
|
||||
|
||||
it('sets data retention to infinite', async () => {
|
||||
const { body } = await supertest
|
||||
.put(`${API_BASE_PATH}/data_streams/${testDataStreamName}/data_retention`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({})
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete', () => {
|
||||
const testDataStreamName1 = 'test-data-stream1';
|
||||
const testDataStreamName2 = 'test-data-stream2';
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
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 security = getService('security');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
const TEST_DS_NAME = 'test-ds-1';
|
||||
|
||||
describe('Data streams tab', function () {
|
||||
before(async () => {
|
||||
await log.debug('Creating required data stream');
|
||||
try {
|
||||
await dataStreams.createDataStream(
|
||||
TEST_DS_NAME,
|
||||
{
|
||||
'@timestamp': {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
} catch (e) {
|
||||
log.debug('[Setup error] Error creating test data stream');
|
||||
throw e;
|
||||
}
|
||||
|
||||
await log.debug('Navigating to the data streams tab');
|
||||
await security.testUser.setRoles(['index_management_user']);
|
||||
await pageObjects.common.navigateToApp('indexManagement');
|
||||
// Navigate to the data streams tab
|
||||
await pageObjects.indexManagement.changeTabs('data_streamsTab');
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await log.debug('Cleaning up created data stream');
|
||||
|
||||
try {
|
||||
await dataStreams.deleteDataStream(TEST_DS_NAME);
|
||||
} catch (e) {
|
||||
log.debug('[Teardown error] Error deleting test data stream');
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
it('shows the details flyout when clicking on a data stream', async () => {
|
||||
// Open details flyout
|
||||
await pageObjects.indexManagement.clickDataStreamAt(0);
|
||||
// Verify url is stateful
|
||||
const url = await browser.getCurrentUrl();
|
||||
expect(url).to.contain(`/data_streams/${TEST_DS_NAME}`);
|
||||
// Assert that flyout is opened
|
||||
expect(await testSubjects.exists('dataStreamDetailPanel')).to.be(true);
|
||||
// Close flyout
|
||||
await testSubjects.click('closeDetailsButton');
|
||||
});
|
||||
|
||||
it('allows to update 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('infiniteRetentionPeriod > input');
|
||||
// Set the retention to 7 hours
|
||||
await testSubjects.setValue('dataRetentionValue', '7');
|
||||
await testSubjects.click('show-filters-button');
|
||||
await testSubjects.click('filter-option-h');
|
||||
|
||||
// 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 updated');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ loadTestFile }: FtrProviderContext) => {
|
||||
describe('Index Management: data streams tab', function () {
|
||||
loadTestFile(require.resolve('./data_streams_tab'));
|
||||
});
|
||||
};
|
|
@ -15,5 +15,6 @@ export default ({ loadTestFile }: FtrProviderContext) => {
|
|||
loadTestFile(require.resolve('./index_details_page'));
|
||||
loadTestFile(require.resolve('./enrich_policies_tab'));
|
||||
loadTestFile(require.resolve('./create_enrich_policy'));
|
||||
loadTestFile(require.resolve('./data_streams_tab'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -34,6 +34,11 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext)
|
|||
await policyDetailsLinks[indexOfRow].click();
|
||||
},
|
||||
|
||||
async clickDataStreamAt(indexOfRow: number): Promise<void> {
|
||||
const dataStreamLinks = await testSubjects.findAll('nameLink');
|
||||
await dataStreamLinks[indexOfRow].click();
|
||||
},
|
||||
|
||||
async clickDeleteEnrichPolicyAt(indexOfRow: number): Promise<void> {
|
||||
const deleteButons = await testSubjects.findAll('deletePolicyButton');
|
||||
await deleteButons[indexOfRow].click();
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const pageObjects = getPageObjects(['svlCommonPage', 'common', 'indexManagement', 'header']);
|
||||
const browser = getService('browser');
|
||||
const security = getService('security');
|
||||
const log = getService('log');
|
||||
const es = getService('es');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const toasts = getService('toasts');
|
||||
|
||||
const TEST_DS_NAME = 'test-ds-1';
|
||||
|
||||
describe('Data Streams', function () {
|
||||
before(async () => {
|
||||
await log.debug('Creating required data stream');
|
||||
try {
|
||||
await es.cluster.putComponentTemplate({
|
||||
name: `${TEST_DS_NAME}_mapping`,
|
||||
template: {
|
||||
settings: { mode: undefined },
|
||||
mappings: {
|
||||
properties: {
|
||||
'@timestamp': {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await es.indices.putIndexTemplate({
|
||||
name: `${TEST_DS_NAME}_index_template`,
|
||||
index_patterns: [TEST_DS_NAME],
|
||||
data_stream: {},
|
||||
composed_of: [`${TEST_DS_NAME}_mapping`],
|
||||
_meta: {
|
||||
description: `Template for ${TEST_DS_NAME} testing index`,
|
||||
},
|
||||
});
|
||||
|
||||
await es.indices.createDataStream({
|
||||
name: TEST_DS_NAME,
|
||||
});
|
||||
} catch (e) {
|
||||
log.debug('[Setup error] Error creating test data stream');
|
||||
throw e;
|
||||
}
|
||||
|
||||
await security.testUser.setRoles(['index_management_user']);
|
||||
// Navigate to the index management page
|
||||
await pageObjects.svlCommonPage.login();
|
||||
await pageObjects.common.navigateToApp('indexManagement');
|
||||
// Navigate to the indices tab
|
||||
await pageObjects.indexManagement.changeTabs('data_streamsTab');
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await log.debug('Cleaning up created data stream');
|
||||
|
||||
try {
|
||||
await es.indices.deleteDataStream({ name: TEST_DS_NAME });
|
||||
await es.indices.deleteIndexTemplate({
|
||||
name: `${TEST_DS_NAME}_index_template`,
|
||||
});
|
||||
await es.cluster.deleteComponentTemplate({
|
||||
name: `${TEST_DS_NAME}_mapping`,
|
||||
});
|
||||
} catch (e) {
|
||||
log.debug('[Teardown error] Error deleting test data stream');
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
it('renders the data streams tab', async () => {
|
||||
const url = await browser.getCurrentUrl();
|
||||
expect(url).to.contain(`/data_streams`);
|
||||
});
|
||||
|
||||
it('shows the details flyout when clicking on a data stream', async () => {
|
||||
// Open details flyout
|
||||
await pageObjects.indexManagement.clickDataStreamAt(0);
|
||||
// Verify url is stateful
|
||||
const url = await browser.getCurrentUrl();
|
||||
expect(url).to.contain(`/data_streams/${TEST_DS_NAME}`);
|
||||
// Assert that flyout is opened
|
||||
expect(await testSubjects.exists('dataStreamDetailPanel')).to.be(true);
|
||||
// Close flyout
|
||||
await testSubjects.click('closeDetailsButton');
|
||||
});
|
||||
|
||||
it('allows to update 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('infiniteRetentionPeriod > input');
|
||||
// Set the retention to 7 hours
|
||||
await testSubjects.setValue('dataRetentionValue', '7');
|
||||
await testSubjects.click('show-filters-button');
|
||||
await testSubjects.click('filter-option-h');
|
||||
|
||||
// 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 updated');
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue