Add delete data stream action and detail panel (#68919)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
bcc62095f0
commit
b48c8bf355
|
@ -35,6 +35,22 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setLoadDataStreamResponse = (response: HttpResponse = []) => {
|
||||||
|
server.respondWith('GET', `${API_BASE_PATH}/data_streams/:id`, [
|
||||||
|
200,
|
||||||
|
{ 'Content-Type': 'application/json' },
|
||||||
|
JSON.stringify(response),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDeleteDataStreamResponse = (response: HttpResponse = []) => {
|
||||||
|
server.respondWith('POST', `${API_BASE_PATH}/delete_data_streams`, [
|
||||||
|
200,
|
||||||
|
{ 'Content-Type': 'application/json' },
|
||||||
|
JSON.stringify(response),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
const setDeleteTemplateResponse = (response: HttpResponse = []) => {
|
const setDeleteTemplateResponse = (response: HttpResponse = []) => {
|
||||||
server.respondWith('POST', `${API_BASE_PATH}/delete_index_templates`, [
|
server.respondWith('POST', `${API_BASE_PATH}/delete_index_templates`, [
|
||||||
200,
|
200,
|
||||||
|
@ -80,6 +96,8 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
|
||||||
setLoadTemplatesResponse,
|
setLoadTemplatesResponse,
|
||||||
setLoadIndicesResponse,
|
setLoadIndicesResponse,
|
||||||
setLoadDataStreamsResponse,
|
setLoadDataStreamsResponse,
|
||||||
|
setLoadDataStreamResponse,
|
||||||
|
setDeleteDataStreamResponse,
|
||||||
setDeleteTemplateResponse,
|
setDeleteTemplateResponse,
|
||||||
setLoadTemplateResponse,
|
setLoadTemplateResponse,
|
||||||
setCreateTemplateResponse,
|
setCreateTemplateResponse,
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
||||||
|
import { merge } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
notificationServiceMock,
|
notificationServiceMock,
|
||||||
|
@ -33,7 +34,7 @@ export const services = {
|
||||||
services.uiMetricService.setup({ reportUiStats() {} } as any);
|
services.uiMetricService.setup({ reportUiStats() {} } as any);
|
||||||
setExtensionsService(services.extensionsService);
|
setExtensionsService(services.extensionsService);
|
||||||
setUiMetricService(services.uiMetricService);
|
setUiMetricService(services.uiMetricService);
|
||||||
const appDependencies = { services, core: {}, plugins: {} } as any;
|
const appDependencies = { services, core: { getUrlForApp: () => {} }, plugins: {} } as any;
|
||||||
|
|
||||||
export const setupEnvironment = () => {
|
export const setupEnvironment = () => {
|
||||||
// Mock initialization of services
|
// Mock initialization of services
|
||||||
|
@ -51,8 +52,13 @@ export const setupEnvironment = () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithAppDependencies = (Comp: any) => (props: any) => (
|
export const WithAppDependencies = (Comp: any, overridingDependencies: any = {}) => (
|
||||||
<AppContextProvider value={appDependencies}>
|
props: any
|
||||||
<Comp {...props} />
|
) => {
|
||||||
</AppContextProvider>
|
const mergedDependencies = merge({}, appDependencies, overridingDependencies);
|
||||||
);
|
return (
|
||||||
|
<AppContextProvider value={mergedDependencies}>
|
||||||
|
<Comp {...props} />
|
||||||
|
</AppContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { ReactWrapper } from 'enzyme';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
registerTestBed,
|
registerTestBed,
|
||||||
|
@ -17,27 +18,38 @@ import { IndexManagementHome } from '../../../public/application/sections/home';
|
||||||
import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths
|
||||||
import { WithAppDependencies, services, TestSubjects } from '../helpers';
|
import { WithAppDependencies, services, TestSubjects } from '../helpers';
|
||||||
|
|
||||||
const testBedConfig: TestBedConfig = {
|
|
||||||
store: () => indexManagementStore(services as any),
|
|
||||||
memoryRouter: {
|
|
||||||
initialEntries: [`/indices`],
|
|
||||||
componentRoutePath: `/:section(indices|data_streams|templates)`,
|
|
||||||
},
|
|
||||||
doMountAsync: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig);
|
|
||||||
|
|
||||||
export interface DataStreamsTabTestBed extends TestBed<TestSubjects> {
|
export interface DataStreamsTabTestBed extends TestBed<TestSubjects> {
|
||||||
actions: {
|
actions: {
|
||||||
goToDataStreamsList: () => void;
|
goToDataStreamsList: () => void;
|
||||||
clickEmptyPromptIndexTemplateLink: () => void;
|
clickEmptyPromptIndexTemplateLink: () => void;
|
||||||
clickReloadButton: () => void;
|
clickReloadButton: () => void;
|
||||||
|
clickNameAt: (index: number) => void;
|
||||||
clickIndicesAt: (index: number) => void;
|
clickIndicesAt: (index: number) => void;
|
||||||
|
clickDeletActionAt: (index: number) => void;
|
||||||
|
clickConfirmDelete: () => void;
|
||||||
|
clickDeletDataStreamButton: () => void;
|
||||||
};
|
};
|
||||||
|
findDeleteActionAt: (index: number) => ReactWrapper;
|
||||||
|
findDeleteConfirmationModal: () => ReactWrapper;
|
||||||
|
findDetailPanel: () => ReactWrapper;
|
||||||
|
findDetailPanelTitle: () => string;
|
||||||
|
findEmptyPromptIndexTemplateLink: () => ReactWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setup = async (): Promise<DataStreamsTabTestBed> => {
|
export const setup = async (overridingDependencies: any = {}): Promise<DataStreamsTabTestBed> => {
|
||||||
|
const testBedConfig: TestBedConfig = {
|
||||||
|
store: () => indexManagementStore(services as any),
|
||||||
|
memoryRouter: {
|
||||||
|
initialEntries: [`/indices`],
|
||||||
|
componentRoutePath: `/:section(indices|data_streams|templates)`,
|
||||||
|
},
|
||||||
|
doMountAsync: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initTestBed = registerTestBed(
|
||||||
|
WithAppDependencies(IndexManagementHome, overridingDependencies),
|
||||||
|
testBedConfig
|
||||||
|
);
|
||||||
const testBed = await initTestBed();
|
const testBed = await initTestBed();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,15 +60,17 @@ export const setup = async (): Promise<DataStreamsTabTestBed> => {
|
||||||
testBed.find('data_streamsTab').simulate('click');
|
testBed.find('data_streamsTab').simulate('click');
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickEmptyPromptIndexTemplateLink = async () => {
|
const findEmptyPromptIndexTemplateLink = () => {
|
||||||
const { find, component, router } = testBed;
|
const { find } = testBed;
|
||||||
|
|
||||||
const templateLink = find('dataStreamsEmptyPromptTemplateLink');
|
const templateLink = find('dataStreamsEmptyPromptTemplateLink');
|
||||||
|
return templateLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickEmptyPromptIndexTemplateLink = async () => {
|
||||||
|
const { component, router } = testBed;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
router.navigateTo(templateLink.props().href!);
|
router.navigateTo(findEmptyPromptIndexTemplateLink().props().href!);
|
||||||
});
|
});
|
||||||
|
|
||||||
component.update();
|
component.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,10 +79,15 @@ export const setup = async (): Promise<DataStreamsTabTestBed> => {
|
||||||
find('reloadButton').simulate('click');
|
find('reloadButton').simulate('click');
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickIndicesAt = async (index: number) => {
|
const findTestSubjectAt = (testSubject: string, index: number) => {
|
||||||
const { component, table, router } = testBed;
|
const { table } = testBed;
|
||||||
const { rows } = table.getMetaData('dataStreamTable');
|
const { rows } = table.getMetaData('dataStreamTable');
|
||||||
const indicesLink = findTestSubject(rows[index].reactWrapper, 'indicesLink');
|
return findTestSubject(rows[index].reactWrapper, testSubject);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickIndicesAt = async (index: number) => {
|
||||||
|
const { component, router } = testBed;
|
||||||
|
const indicesLink = findTestSubjectAt('indicesLink', index);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
router.navigateTo(indicesLink.props().href!);
|
router.navigateTo(indicesLink.props().href!);
|
||||||
|
@ -77,14 +96,71 @@ export const setup = async (): Promise<DataStreamsTabTestBed> => {
|
||||||
component.update();
|
component.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clickNameAt = async (index: number) => {
|
||||||
|
const { component, router } = testBed;
|
||||||
|
const nameLink = findTestSubjectAt('nameLink', index);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
router.navigateTo(nameLink.props().href!);
|
||||||
|
});
|
||||||
|
|
||||||
|
component.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDeleteActionAt = findTestSubjectAt.bind(null, 'deleteDataStream');
|
||||||
|
|
||||||
|
const clickDeletActionAt = (index: number) => {
|
||||||
|
findDeleteActionAt(index).simulate('click');
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDeleteConfirmationModal = () => {
|
||||||
|
const { find } = testBed;
|
||||||
|
return find('deleteDataStreamsConfirmation');
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickConfirmDelete = async () => {
|
||||||
|
const modal = document.body.querySelector('[data-test-subj="deleteDataStreamsConfirmation"]');
|
||||||
|
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
|
||||||
|
'[data-test-subj="confirmModalConfirmButton"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
confirmButton!.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickDeletDataStreamButton = () => {
|
||||||
|
const { find } = testBed;
|
||||||
|
find('deleteDataStreamButton').simulate('click');
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDetailPanel = () => {
|
||||||
|
const { find } = testBed;
|
||||||
|
return find('dataStreamDetailPanel');
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDetailPanelTitle = () => {
|
||||||
|
const { find } = testBed;
|
||||||
|
return find('dataStreamDetailPanelTitle').text();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...testBed,
|
...testBed,
|
||||||
actions: {
|
actions: {
|
||||||
goToDataStreamsList,
|
goToDataStreamsList,
|
||||||
clickEmptyPromptIndexTemplateLink,
|
clickEmptyPromptIndexTemplateLink,
|
||||||
clickReloadButton,
|
clickReloadButton,
|
||||||
|
clickNameAt,
|
||||||
clickIndicesAt,
|
clickIndicesAt,
|
||||||
|
clickDeletActionAt,
|
||||||
|
clickConfirmDelete,
|
||||||
|
clickDeletDataStreamButton,
|
||||||
},
|
},
|
||||||
|
findDeleteActionAt,
|
||||||
|
findDeleteConfirmationModal,
|
||||||
|
findDetailPanel,
|
||||||
|
findDetailPanelTitle,
|
||||||
|
findEmptyPromptIndexTemplateLink,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,61 +19,38 @@ describe('Data Streams tab', () => {
|
||||||
server.restore();
|
server.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
httpRequestsMockHelpers.setLoadIndicesResponse([
|
|
||||||
{
|
|
||||||
health: '',
|
|
||||||
status: '',
|
|
||||||
primary: '',
|
|
||||||
replica: '',
|
|
||||||
documents: '',
|
|
||||||
documents_deleted: '',
|
|
||||||
size: '',
|
|
||||||
primary_size: '',
|
|
||||||
name: 'data-stream-index',
|
|
||||||
data_stream: 'dataStream1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
health: 'green',
|
|
||||||
status: 'open',
|
|
||||||
primary: 1,
|
|
||||||
replica: 1,
|
|
||||||
documents: 10000,
|
|
||||||
documents_deleted: 100,
|
|
||||||
size: '156kb',
|
|
||||||
primary_size: '156kb',
|
|
||||||
name: 'non-data-stream-index',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
testBed = await setup();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when there are no data streams', () => {
|
describe('when there are no data streams', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const { actions, component } = testBed;
|
httpRequestsMockHelpers.setLoadIndicesResponse([]);
|
||||||
|
|
||||||
httpRequestsMockHelpers.setLoadDataStreamsResponse([]);
|
httpRequestsMockHelpers.setLoadDataStreamsResponse([]);
|
||||||
httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] });
|
httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] });
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
actions.goToDataStreamsList();
|
|
||||||
});
|
|
||||||
|
|
||||||
component.update();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('displays an empty prompt', async () => {
|
test('displays an empty prompt', async () => {
|
||||||
const { exists } = testBed;
|
testBed = await setup();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
testBed.actions.goToDataStreamsList();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { exists, component } = testBed;
|
||||||
|
component.update();
|
||||||
|
|
||||||
expect(exists('sectionLoading')).toBe(false);
|
expect(exists('sectionLoading')).toBe(false);
|
||||||
expect(exists('emptyPrompt')).toBe(true);
|
expect(exists('emptyPrompt')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('goes to index templates tab when "Get started" link is clicked', async () => {
|
test('when Ingest Manager is disabled, goes to index templates tab when "Get started" link is clicked', async () => {
|
||||||
const { actions, exists } = testBed;
|
testBed = await setup({
|
||||||
|
plugins: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
testBed.actions.goToDataStreamsList();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { actions, exists, component } = testBed;
|
||||||
|
component.update();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
actions.clickEmptyPromptIndexTemplateLink();
|
actions.clickEmptyPromptIndexTemplateLink();
|
||||||
|
@ -81,32 +58,77 @@ describe('Data Streams tab', () => {
|
||||||
|
|
||||||
expect(exists('templateList')).toBe(true);
|
expect(exists('templateList')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('when Ingest Manager is enabled, links to Ingest Manager', async () => {
|
||||||
|
testBed = await setup({
|
||||||
|
plugins: { ingestManager: { hi: 'ok' } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
testBed.actions.goToDataStreamsList();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { findEmptyPromptIndexTemplateLink, component } = testBed;
|
||||||
|
component.update();
|
||||||
|
|
||||||
|
// Assert against the text because the href won't be available, due to dependency upon our core mock.
|
||||||
|
expect(findEmptyPromptIndexTemplateLink().text()).toBe('Ingest Manager');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when there are data streams', () => {
|
describe('when there are data streams', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const { actions, component } = testBed;
|
httpRequestsMockHelpers.setLoadIndicesResponse([
|
||||||
|
{
|
||||||
|
health: '',
|
||||||
|
status: '',
|
||||||
|
primary: '',
|
||||||
|
replica: '',
|
||||||
|
documents: '',
|
||||||
|
documents_deleted: '',
|
||||||
|
size: '',
|
||||||
|
primary_size: '',
|
||||||
|
name: 'data-stream-index',
|
||||||
|
data_stream: 'dataStream1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
health: 'green',
|
||||||
|
status: 'open',
|
||||||
|
primary: 1,
|
||||||
|
replica: 1,
|
||||||
|
documents: 10000,
|
||||||
|
documents_deleted: 100,
|
||||||
|
size: '156kb',
|
||||||
|
primary_size: '156kb',
|
||||||
|
name: 'non-data-stream-index',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dataStreamForDetailPanel = createDataStreamPayload('dataStream1');
|
||||||
|
|
||||||
httpRequestsMockHelpers.setLoadDataStreamsResponse([
|
httpRequestsMockHelpers.setLoadDataStreamsResponse([
|
||||||
createDataStreamPayload('dataStream1'),
|
dataStreamForDetailPanel,
|
||||||
createDataStreamPayload('dataStream2'),
|
createDataStreamPayload('dataStream2'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
httpRequestsMockHelpers.setLoadDataStreamResponse(dataStreamForDetailPanel);
|
||||||
|
|
||||||
|
testBed = await setup();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
actions.goToDataStreamsList();
|
testBed.actions.goToDataStreamsList();
|
||||||
});
|
});
|
||||||
|
|
||||||
component.update();
|
testBed.component.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('lists them in the table', async () => {
|
test('lists them in the table', async () => {
|
||||||
const { table } = testBed;
|
const { table } = testBed;
|
||||||
|
|
||||||
const { tableCellsValues } = table.getMetaData('dataStreamTable');
|
const { tableCellsValues } = table.getMetaData('dataStreamTable');
|
||||||
|
|
||||||
expect(tableCellsValues).toEqual([
|
expect(tableCellsValues).toEqual([
|
||||||
['dataStream1', '1', '@timestamp', '1'],
|
['', 'dataStream1', '1', ''],
|
||||||
['dataStream2', '1', '@timestamp', '1'],
|
['', 'dataStream2', '1', ''],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -126,12 +148,90 @@ describe('Data Streams tab', () => {
|
||||||
|
|
||||||
test('clicking the indices count navigates to the backing indices', async () => {
|
test('clicking the indices count navigates to the backing indices', async () => {
|
||||||
const { table, actions } = testBed;
|
const { table, actions } = testBed;
|
||||||
|
|
||||||
await actions.clickIndicesAt(0);
|
await actions.clickIndicesAt(0);
|
||||||
|
|
||||||
expect(table.getMetaData('indexTable').tableCellsValues).toEqual([
|
expect(table.getMetaData('indexTable').tableCellsValues).toEqual([
|
||||||
['', '', '', '', '', '', '', 'dataStream1'],
|
['', '', '', '', '', '', '', 'dataStream1'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('row actions', () => {
|
||||||
|
test('can delete', () => {
|
||||||
|
const { findDeleteActionAt } = testBed;
|
||||||
|
const deleteAction = findDeleteActionAt(0);
|
||||||
|
expect(deleteAction.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleting a data stream', () => {
|
||||||
|
test('shows a confirmation modal', async () => {
|
||||||
|
const {
|
||||||
|
actions: { clickDeletActionAt },
|
||||||
|
findDeleteConfirmationModal,
|
||||||
|
} = testBed;
|
||||||
|
clickDeletActionAt(0);
|
||||||
|
const confirmationModal = findDeleteConfirmationModal();
|
||||||
|
expect(confirmationModal).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends a request to the Delete API', async () => {
|
||||||
|
const {
|
||||||
|
actions: { clickDeletActionAt, clickConfirmDelete },
|
||||||
|
} = testBed;
|
||||||
|
clickDeletActionAt(0);
|
||||||
|
|
||||||
|
httpRequestsMockHelpers.setDeleteDataStreamResponse({
|
||||||
|
results: {
|
||||||
|
dataStreamsDeleted: ['dataStream1'],
|
||||||
|
errors: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await clickConfirmDelete();
|
||||||
|
|
||||||
|
const { method, url, requestBody } = server.requests[server.requests.length - 1];
|
||||||
|
|
||||||
|
expect(method).toBe('POST');
|
||||||
|
expect(url).toBe(`${API_BASE_PATH}/delete_data_streams`);
|
||||||
|
expect(JSON.parse(JSON.parse(requestBody).body)).toEqual({
|
||||||
|
dataStreams: ['dataStream1'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detail panel', () => {
|
||||||
|
test('opens when the data stream name in the table is clicked', async () => {
|
||||||
|
const { actions, findDetailPanel, findDetailPanelTitle } = testBed;
|
||||||
|
await actions.clickNameAt(0);
|
||||||
|
expect(findDetailPanel().length).toBe(1);
|
||||||
|
expect(findDetailPanelTitle()).toBe('dataStream1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deletes the data stream when delete button is clicked', async () => {
|
||||||
|
const {
|
||||||
|
actions: { clickNameAt, clickDeletDataStreamButton, clickConfirmDelete },
|
||||||
|
} = testBed;
|
||||||
|
|
||||||
|
await clickNameAt(0);
|
||||||
|
|
||||||
|
clickDeletDataStreamButton();
|
||||||
|
|
||||||
|
httpRequestsMockHelpers.setDeleteDataStreamResponse({
|
||||||
|
results: {
|
||||||
|
dataStreamsDeleted: ['dataStream1'],
|
||||||
|
errors: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await clickConfirmDelete();
|
||||||
|
|
||||||
|
const { method, url, requestBody } = server.requests[server.requests.length - 1];
|
||||||
|
|
||||||
|
expect(method).toBe('POST');
|
||||||
|
expect(url).toBe(`${API_BASE_PATH}/delete_data_streams`);
|
||||||
|
expect(JSON.parse(JSON.parse(requestBody).body)).toEqual({
|
||||||
|
dataStreams: ['dataStream1'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { ReactWrapper } from 'enzyme';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
registerTestBed,
|
registerTestBed,
|
||||||
|
@ -34,6 +35,8 @@ export interface IndicesTestBed extends TestBed<TestSubjects> {
|
||||||
clickIncludeHiddenIndicesToggle: () => void;
|
clickIncludeHiddenIndicesToggle: () => void;
|
||||||
clickDataStreamAt: (index: number) => void;
|
clickDataStreamAt: (index: number) => void;
|
||||||
};
|
};
|
||||||
|
findDataStreamDetailPanel: () => ReactWrapper;
|
||||||
|
findDataStreamDetailPanelTitle: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setup = async (): Promise<IndicesTestBed> => {
|
export const setup = async (): Promise<IndicesTestBed> => {
|
||||||
|
@ -77,6 +80,16 @@ export const setup = async (): Promise<IndicesTestBed> => {
|
||||||
component.update();
|
component.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findDataStreamDetailPanel = () => {
|
||||||
|
const { find } = testBed;
|
||||||
|
return find('dataStreamDetailPanel');
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDataStreamDetailPanelTitle = () => {
|
||||||
|
const { find } = testBed;
|
||||||
|
return find('dataStreamDetailPanelTitle').text();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...testBed,
|
...testBed,
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -85,5 +98,7 @@ export const setup = async (): Promise<IndicesTestBed> => {
|
||||||
clickIncludeHiddenIndicesToggle,
|
clickIncludeHiddenIndicesToggle,
|
||||||
clickDataStreamAt,
|
clickDataStreamAt,
|
||||||
},
|
},
|
||||||
|
findDataStreamDetailPanel,
|
||||||
|
findDataStreamDetailPanelTitle,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -70,10 +70,10 @@ describe('<IndexManagementHome />', () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
httpRequestsMockHelpers.setLoadDataStreamsResponse([
|
// The detail panel should still appear even if there are no data streams.
|
||||||
createDataStreamPayload('dataStream1'),
|
httpRequestsMockHelpers.setLoadDataStreamsResponse([]);
|
||||||
createDataStreamPayload('dataStream2'),
|
|
||||||
]);
|
httpRequestsMockHelpers.setLoadDataStreamResponse(createDataStreamPayload('dataStream1'));
|
||||||
|
|
||||||
testBed = await setup();
|
testBed = await setup();
|
||||||
|
|
||||||
|
@ -86,13 +86,16 @@ describe('<IndexManagementHome />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('navigates to the data stream in the Data Streams tab', async () => {
|
test('navigates to the data stream in the Data Streams tab', async () => {
|
||||||
const { table, actions } = testBed;
|
const {
|
||||||
|
findDataStreamDetailPanel,
|
||||||
|
findDataStreamDetailPanelTitle,
|
||||||
|
actions: { clickDataStreamAt },
|
||||||
|
} = testBed;
|
||||||
|
|
||||||
await actions.clickDataStreamAt(0);
|
await clickDataStreamAt(0);
|
||||||
|
|
||||||
expect(table.getMetaData('dataStreamTable').tableCellsValues).toEqual([
|
expect(findDataStreamDetailPanel().length).toBe(1);
|
||||||
['dataStream1', '1', '@timestamp', '1'],
|
expect(findDataStreamDetailPanelTitle()).toBe('dataStream1');
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
|
|
||||||
import { DataStream, DataStreamFromEs } from '../types';
|
import { DataStream, DataStreamFromEs } from '../types';
|
||||||
|
|
||||||
export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]): DataStream[] {
|
export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataStream {
|
||||||
return dataStreamsFromEs.map(({ name, timestamp_field, indices, generation }) => ({
|
const { name, timestamp_field, indices, generation } = dataStreamFromEs;
|
||||||
|
|
||||||
|
return {
|
||||||
name,
|
name,
|
||||||
timeStampField: timestamp_field,
|
timeStampField: timestamp_field,
|
||||||
indices: indices.map(
|
indices: indices.map(
|
||||||
|
@ -17,5 +19,9 @@ export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[])
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
generation,
|
generation,
|
||||||
}));
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]): DataStream[] {
|
||||||
|
return dataStreamsFromEs.map((dataStream) => deserializeDataStream(dataStream));
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { deserializeDataStreamList } from './data_stream_serialization';
|
export { deserializeDataStream, deserializeDataStreamList } from './data_stream_serialization';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
deserializeLegacyTemplateList,
|
deserializeLegacyTemplateList,
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
],
|
],
|
||||||
"optionalPlugins": [
|
"optionalPlugins": [
|
||||||
"security",
|
"security",
|
||||||
"usageCollection"
|
"usageCollection",
|
||||||
|
"ingestManager"
|
||||||
],
|
],
|
||||||
"configPath": ["xpack", "index_management"]
|
"configPath": ["xpack", "index_management"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
|
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { ScopedHistory } from 'kibana/public';
|
import { ScopedHistory } from 'kibana/public';
|
||||||
import { CoreStart } from '../../../../../src/core/public';
|
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||||
|
|
||||||
import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public';
|
import { CoreStart } from '../../../../../src/core/public';
|
||||||
|
import { IngestManagerSetup } from '../../../ingest_manager/public';
|
||||||
import { IndexMgmtMetricsType } from '../types';
|
import { IndexMgmtMetricsType } from '../types';
|
||||||
import { UiMetricService, NotificationService, HttpService } from './services';
|
import { UiMetricService, NotificationService, HttpService } from './services';
|
||||||
import { ExtensionsService } from '../services';
|
import { ExtensionsService } from '../services';
|
||||||
|
@ -22,6 +23,7 @@ export interface AppDependencies {
|
||||||
};
|
};
|
||||||
plugins: {
|
plugins: {
|
||||||
usageCollection: UsageCollectionSetup;
|
usageCollection: UsageCollectionSetup;
|
||||||
|
ingestManager?: IngestManagerSetup;
|
||||||
};
|
};
|
||||||
services: {
|
services: {
|
||||||
uiMetricService: UiMetricService<IndexMgmtMetricsType>;
|
uiMetricService: UiMetricService<IndexMgmtMetricsType>;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { CoreSetup } from 'src/core/public';
|
||||||
import { ManagementAppMountParams } from 'src/plugins/management/public/';
|
import { ManagementAppMountParams } from 'src/plugins/management/public/';
|
||||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||||
|
|
||||||
|
import { IngestManagerSetup } from '../../../ingest_manager/public';
|
||||||
import { ExtensionsService } from '../services';
|
import { ExtensionsService } from '../services';
|
||||||
import { IndexMgmtMetricsType } from '../types';
|
import { IndexMgmtMetricsType } from '../types';
|
||||||
import { AppDependencies } from './app_context';
|
import { AppDependencies } from './app_context';
|
||||||
|
@ -28,7 +29,8 @@ export async function mountManagementSection(
|
||||||
coreSetup: CoreSetup,
|
coreSetup: CoreSetup,
|
||||||
usageCollection: UsageCollectionSetup,
|
usageCollection: UsageCollectionSetup,
|
||||||
services: InternalServices,
|
services: InternalServices,
|
||||||
params: ManagementAppMountParams
|
params: ManagementAppMountParams,
|
||||||
|
ingestManager?: IngestManagerSetup
|
||||||
) {
|
) {
|
||||||
const { element, setBreadcrumbs, history } = params;
|
const { element, setBreadcrumbs, history } = params;
|
||||||
const [core] = await coreSetup.getStartServices();
|
const [core] = await coreSetup.getStartServices();
|
||||||
|
@ -44,6 +46,7 @@ export async function mountManagementSection(
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
usageCollection,
|
usageCollection,
|
||||||
|
ingestManager,
|
||||||
},
|
},
|
||||||
services,
|
services,
|
||||||
history,
|
history,
|
||||||
|
|
|
@ -4,9 +4,10 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import {
|
import {
|
||||||
|
EuiButton,
|
||||||
EuiFlyout,
|
EuiFlyout,
|
||||||
EuiFlyoutHeader,
|
EuiFlyoutHeader,
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
|
@ -15,14 +16,18 @@ import {
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
EuiButtonEmpty,
|
EuiButtonEmpty,
|
||||||
|
EuiDescriptionList,
|
||||||
|
EuiDescriptionListTitle,
|
||||||
|
EuiDescriptionListDescription,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { SectionLoading, SectionError, Error } from '../../../../components';
|
import { SectionLoading, SectionError, Error } from '../../../../components';
|
||||||
import { useLoadDataStream } from '../../../../services/api';
|
import { useLoadDataStream } from '../../../../services/api';
|
||||||
|
import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dataStreamName: string;
|
dataStreamName: string;
|
||||||
onClose: () => void;
|
onClose: (shouldReload?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,6 +41,8 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName);
|
const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName);
|
||||||
|
|
||||||
|
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
@ -61,44 +68,97 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (dataStream) {
|
} else if (dataStream) {
|
||||||
content = <Fragment>{JSON.stringify(dataStream)}</Fragment>;
|
const { timeStampField, generation } = dataStream;
|
||||||
|
|
||||||
|
content = (
|
||||||
|
<EuiDescriptionList textStyle="reverse">
|
||||||
|
<EuiDescriptionListTitle>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.dataStreamDetailPanel.timestampFieldTitle"
|
||||||
|
defaultMessage="Timestamp field"
|
||||||
|
/>
|
||||||
|
</EuiDescriptionListTitle>
|
||||||
|
|
||||||
|
<EuiDescriptionListDescription>{timeStampField.name}</EuiDescriptionListDescription>
|
||||||
|
|
||||||
|
<EuiDescriptionListTitle>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.dataStreamDetailPanel.generationTitle"
|
||||||
|
defaultMessage="Generation"
|
||||||
|
/>
|
||||||
|
</EuiDescriptionListTitle>
|
||||||
|
|
||||||
|
<EuiDescriptionListDescription>{generation}</EuiDescriptionListDescription>
|
||||||
|
</EuiDescriptionList>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlyout
|
<>
|
||||||
onClose={onClose}
|
{isDeleting ? (
|
||||||
data-test-subj="dataStreamDetailPanel"
|
<DeleteDataStreamConfirmationModal
|
||||||
aria-labelledby="dataStreamDetailPanelTitle"
|
onClose={(data) => {
|
||||||
size="m"
|
if (data && data.hasDeletedDataStreams) {
|
||||||
maxWidth={500}
|
onClose(true);
|
||||||
>
|
} else {
|
||||||
<EuiFlyoutHeader>
|
setIsDeleting(false);
|
||||||
<EuiTitle size="m">
|
}
|
||||||
<h2 id="dataStreamDetailPanelTitle" data-test-subj="title">
|
}}
|
||||||
{dataStreamName}
|
dataStreams={[dataStreamName]}
|
||||||
</h2>
|
/>
|
||||||
</EuiTitle>
|
) : null}
|
||||||
</EuiFlyoutHeader>
|
|
||||||
|
|
||||||
<EuiFlyoutBody data-test-subj="content">{content}</EuiFlyoutBody>
|
<EuiFlyout
|
||||||
|
onClose={onClose}
|
||||||
|
data-test-subj="dataStreamDetailPanel"
|
||||||
|
aria-labelledby="dataStreamDetailPanelTitle"
|
||||||
|
size="m"
|
||||||
|
maxWidth={500}
|
||||||
|
>
|
||||||
|
<EuiFlyoutHeader>
|
||||||
|
<EuiTitle size="m">
|
||||||
|
<h2 id="dataStreamDetailPanelTitle" data-test-subj="dataStreamDetailPanelTitle">
|
||||||
|
{dataStreamName}
|
||||||
|
</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
</EuiFlyoutHeader>
|
||||||
|
|
||||||
<EuiFlyoutFooter>
|
<EuiFlyoutBody data-test-subj="content">{content}</EuiFlyoutBody>
|
||||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlyoutFooter>
|
||||||
<EuiButtonEmpty
|
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||||
iconType="cross"
|
<EuiFlexItem grow={false}>
|
||||||
flush="left"
|
<EuiButtonEmpty
|
||||||
onClick={onClose}
|
iconType="cross"
|
||||||
data-test-subj="closeDetailsButton"
|
flush="left"
|
||||||
>
|
onClick={() => onClose()}
|
||||||
<FormattedMessage
|
data-test-subj="closeDetailsButton"
|
||||||
id="xpack.idxMgmt.dataStreamDetailPanel.closeButtonLabel"
|
>
|
||||||
defaultMessage="Close"
|
<FormattedMessage
|
||||||
/>
|
id="xpack.idxMgmt.dataStreamDetailPanel.closeButtonLabel"
|
||||||
</EuiButtonEmpty>
|
defaultMessage="Close"
|
||||||
</EuiFlexItem>
|
/>
|
||||||
</EuiFlexGroup>
|
</EuiButtonEmpty>
|
||||||
</EuiFlyoutFooter>
|
</EuiFlexItem>
|
||||||
</EuiFlyout>
|
|
||||||
|
{!isLoading && !error ? (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton
|
||||||
|
color="danger"
|
||||||
|
iconType="trash"
|
||||||
|
onClick={() => setIsDeleting(true)}
|
||||||
|
data-test-subj="deleteDataStreamButton"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.dataStreamDetailPanel.deleteButtonLabel"
|
||||||
|
defaultMessage="Delete data stream"
|
||||||
|
/>
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
) : null}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlyoutFooter>
|
||||||
|
</EuiFlyout>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,9 +12,13 @@ import { EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/
|
||||||
import { ScopedHistory } from 'kibana/public';
|
import { ScopedHistory } from 'kibana/public';
|
||||||
|
|
||||||
import { reactRouterNavigate } from '../../../../shared_imports';
|
import { reactRouterNavigate } from '../../../../shared_imports';
|
||||||
|
import { useAppContext } from '../../../app_context';
|
||||||
import { SectionError, SectionLoading, Error } from '../../../components';
|
import { SectionError, SectionLoading, Error } from '../../../components';
|
||||||
import { useLoadDataStreams } from '../../../services/api';
|
import { useLoadDataStreams } from '../../../services/api';
|
||||||
|
import { decodePathFromReactRouter } from '../../../services/routing';
|
||||||
|
import { Section } from '../../home';
|
||||||
import { DataStreamTable } from './data_stream_table';
|
import { DataStreamTable } from './data_stream_table';
|
||||||
|
import { DataStreamDetailPanel } from './data_stream_detail_panel';
|
||||||
|
|
||||||
interface MatchParams {
|
interface MatchParams {
|
||||||
dataStreamName?: string;
|
dataStreamName?: string;
|
||||||
|
@ -26,6 +30,11 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
|
||||||
},
|
},
|
||||||
history,
|
history,
|
||||||
}) => {
|
}) => {
|
||||||
|
const {
|
||||||
|
core: { getUrlForApp },
|
||||||
|
plugins: { ingestManager },
|
||||||
|
} = useAppContext();
|
||||||
|
|
||||||
const { error, isLoading, data: dataStreams, sendRequest: reload } = useLoadDataStreams();
|
const { error, isLoading, data: dataStreams, sendRequest: reload } = useLoadDataStreams();
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
@ -67,22 +76,52 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsDescription"
|
id="xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsDescription"
|
||||||
defaultMessage="Data streams represent the latest data in a rollover series. Get started with data streams by creating a {link}."
|
defaultMessage="Data streams represent collections of time series indices."
|
||||||
values={{
|
|
||||||
link: (
|
|
||||||
<EuiLink
|
|
||||||
data-test-subj="dataStreamsEmptyPromptTemplateLink"
|
|
||||||
{...reactRouterNavigate(history, {
|
|
||||||
pathname: '/templates',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{i18n.translate('xpack.idxMgmt.dataStreamList.emptyPrompt.getStartedLink', {
|
|
||||||
defaultMessage: 'composable index template',
|
|
||||||
})}
|
|
||||||
</EuiLink>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
{' ' /* We need this space to separate these two sentences. */}
|
||||||
|
{ingestManager ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsCtaIngestManagerMessage"
|
||||||
|
defaultMessage="Get started with data streams in {link}."
|
||||||
|
values={{
|
||||||
|
link: (
|
||||||
|
<EuiLink
|
||||||
|
data-test-subj="dataStreamsEmptyPromptTemplateLink"
|
||||||
|
href={getUrlForApp('ingestManager')}
|
||||||
|
>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsCtaIngestManagerLink',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Ingest Manager',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</EuiLink>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsCtaIndexTemplateMessage"
|
||||||
|
defaultMessage="Get started with data streams by creating a {link}."
|
||||||
|
values={{
|
||||||
|
link: (
|
||||||
|
<EuiLink
|
||||||
|
data-test-subj="dataStreamsEmptyPromptTemplateLink"
|
||||||
|
{...reactRouterNavigate(history, {
|
||||||
|
pathname: '/templates',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsCtaIndexTemplateLink',
|
||||||
|
{
|
||||||
|
defaultMessage: 'composable index template',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</EuiLink>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
data-test-subj="emptyPrompt"
|
data-test-subj="emptyPrompt"
|
||||||
|
@ -104,24 +143,38 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
|
|
||||||
<DataStreamTable
|
<DataStreamTable
|
||||||
filters={dataStreamName !== undefined ? `name=${dataStreamName}` : ''}
|
filters={
|
||||||
|
dataStreamName !== undefined ? `name=${decodePathFromReactRouter(dataStreamName)}` : ''
|
||||||
|
}
|
||||||
dataStreams={dataStreams}
|
dataStreams={dataStreams}
|
||||||
reload={reload}
|
reload={reload}
|
||||||
history={history as ScopedHistory}
|
history={history as ScopedHistory}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* TODO: Implement this once we have something to put in here, e.g. storage size, docs count */}
|
|
||||||
{/* dataStreamName && (
|
|
||||||
<DataStreamDetailPanel
|
|
||||||
dataStreamName={decodePathFromReactRouter(dataStreamName)}
|
|
||||||
onClose={() => {
|
|
||||||
history.push('/data_streams');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)*/}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div data-test-subj="dataStreamList">{content}</div>;
|
return (
|
||||||
|
<div data-test-subj="dataStreamList">
|
||||||
|
{content}
|
||||||
|
|
||||||
|
{/*
|
||||||
|
If the user has been deep-linked, they'll expect to see the detail panel because it reflects
|
||||||
|
the URL state, even if there are no data streams or if there was an error loading them.
|
||||||
|
*/}
|
||||||
|
{dataStreamName && (
|
||||||
|
<DataStreamDetailPanel
|
||||||
|
dataStreamName={decodePathFromReactRouter(dataStreamName)}
|
||||||
|
onClose={(shouldReload?: boolean) => {
|
||||||
|
history.push(`/${Section.DataStreams}`);
|
||||||
|
|
||||||
|
// If the data stream was deleted, we need to refresh the list.
|
||||||
|
if (shouldReload) {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink } from '@elastic/eui';
|
import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink } from '@elastic/eui';
|
||||||
|
@ -13,6 +13,8 @@ import { ScopedHistory } from 'kibana/public';
|
||||||
import { DataStream } from '../../../../../../common/types';
|
import { DataStream } from '../../../../../../common/types';
|
||||||
import { reactRouterNavigate } from '../../../../../shared_imports';
|
import { reactRouterNavigate } from '../../../../../shared_imports';
|
||||||
import { encodePathForReactRouter } from '../../../../services/routing';
|
import { encodePathForReactRouter } from '../../../../services/routing';
|
||||||
|
import { Section } from '../../../home';
|
||||||
|
import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dataStreams?: DataStream[];
|
dataStreams?: DataStream[];
|
||||||
|
@ -27,6 +29,9 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
||||||
history,
|
history,
|
||||||
filters,
|
filters,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [selection, setSelection] = useState<DataStream[]>([]);
|
||||||
|
const [dataStreamsToDelete, setDataStreamsToDelete] = useState<string[]>([]);
|
||||||
|
|
||||||
const columns: Array<EuiBasicTableColumn<DataStream>> = [
|
const columns: Array<EuiBasicTableColumn<DataStream>> = [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
|
@ -35,7 +40,19 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
||||||
}),
|
}),
|
||||||
truncateText: true,
|
truncateText: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
// TODO: Render as a link to open the detail panel
|
render: (name: DataStream['name'], item: DataStream) => {
|
||||||
|
return (
|
||||||
|
/* eslint-disable-next-line @elastic/eui/href-or-on-click */
|
||||||
|
<EuiLink
|
||||||
|
data-test-subj="nameLink"
|
||||||
|
{...reactRouterNavigate(history, {
|
||||||
|
pathname: `/${Section.DataStreams}/${encodePathForReactRouter(name)}`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</EuiLink>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'indices',
|
field: 'indices',
|
||||||
|
@ -59,20 +76,27 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'timeStampField.name',
|
name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionColumnTitle', {
|
||||||
name: i18n.translate('xpack.idxMgmt.dataStreamList.table.timeStampFieldColumnTitle', {
|
defaultMessage: 'Actions',
|
||||||
defaultMessage: 'Timestamp field',
|
|
||||||
}),
|
}),
|
||||||
truncateText: true,
|
actions: [
|
||||||
sortable: true,
|
{
|
||||||
},
|
name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteText', {
|
||||||
{
|
defaultMessage: 'Delete',
|
||||||
field: 'generation',
|
}),
|
||||||
name: i18n.translate('xpack.idxMgmt.dataStreamList.table.generationFieldColumnTitle', {
|
description: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteDecription', {
|
||||||
defaultMessage: 'Generation',
|
defaultMessage: 'Delete this data stream',
|
||||||
}),
|
}),
|
||||||
truncateText: true,
|
icon: 'trash',
|
||||||
sortable: true,
|
color: 'danger',
|
||||||
|
type: 'icon',
|
||||||
|
onClick: ({ name }: DataStream) => {
|
||||||
|
setDataStreamsToDelete([name]);
|
||||||
|
},
|
||||||
|
isPrimary: true,
|
||||||
|
'data-test-subj': 'deleteDataStream',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -88,12 +112,29 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const selectionConfig = {
|
||||||
|
onSelectionChange: setSelection,
|
||||||
|
};
|
||||||
|
|
||||||
const searchConfig = {
|
const searchConfig = {
|
||||||
query: filters,
|
query: filters,
|
||||||
box: {
|
box: {
|
||||||
incremental: true,
|
incremental: true,
|
||||||
},
|
},
|
||||||
toolsLeft: undefined /* TODO: Actions menu */,
|
toolsLeft:
|
||||||
|
selection.length > 0 ? (
|
||||||
|
<EuiButton
|
||||||
|
data-test-subj="deletDataStreamsButton"
|
||||||
|
onClick={() => setDataStreamsToDelete(selection.map(({ name }: DataStream) => name))}
|
||||||
|
color="danger"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.dataStreamList.table.deleteDataStreamsButtonLabel"
|
||||||
|
defaultMessage="Delete {count, plural, one {data stream} other {data streams} }"
|
||||||
|
values={{ count: selection.length }}
|
||||||
|
/>
|
||||||
|
</EuiButton>
|
||||||
|
) : undefined,
|
||||||
toolsRight: [
|
toolsRight: [
|
||||||
<EuiButton
|
<EuiButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
@ -112,6 +153,18 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{dataStreamsToDelete && dataStreamsToDelete.length > 0 ? (
|
||||||
|
<DeleteDataStreamConfirmationModal
|
||||||
|
onClose={(data) => {
|
||||||
|
if (data && data.hasDeletedDataStreams) {
|
||||||
|
reload();
|
||||||
|
} else {
|
||||||
|
setDataStreamsToDelete([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
dataStreams={dataStreamsToDelete}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<EuiInMemoryTable
|
<EuiInMemoryTable
|
||||||
items={dataStreams || []}
|
items={dataStreams || []}
|
||||||
itemId="name"
|
itemId="name"
|
||||||
|
@ -119,6 +172,7 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
||||||
search={searchConfig}
|
search={searchConfig}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
isSelectable={true}
|
isSelectable={true}
|
||||||
|
selection={selectionConfig}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
rowProps={() => ({
|
rowProps={() => ({
|
||||||
'data-test-subj': 'row',
|
'data-test-subj': 'row',
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
|
||||||
|
import { deleteDataStreams } from '../../../../services/api';
|
||||||
|
import { notificationService } from '../../../../services/notification';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dataStreams: string[];
|
||||||
|
onClose: (data?: { hasDeletedDataStreams: boolean }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteDataStreamConfirmationModal: React.FunctionComponent<Props> = ({
|
||||||
|
dataStreams,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
dataStreams: string[];
|
||||||
|
onClose: (data?: { hasDeletedDataStreams: boolean }) => void;
|
||||||
|
}) => {
|
||||||
|
const dataStreamsCount = dataStreams.length;
|
||||||
|
|
||||||
|
const handleDeleteDataStreams = () => {
|
||||||
|
deleteDataStreams(dataStreams).then(({ data: { dataStreamsDeleted, errors }, error }) => {
|
||||||
|
const hasDeletedDataStreams = dataStreamsDeleted && dataStreamsDeleted.length;
|
||||||
|
|
||||||
|
if (hasDeletedDataStreams) {
|
||||||
|
const successMessage =
|
||||||
|
dataStreamsDeleted.length === 1
|
||||||
|
? i18n.translate(
|
||||||
|
'xpack.idxMgmt.deleteDataStreamsConfirmationModal.successDeleteSingleNotificationMessageText',
|
||||||
|
{
|
||||||
|
defaultMessage: "Deleted data stream '{dataStreamName}'",
|
||||||
|
values: { dataStreamName: dataStreams[0] },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: i18n.translate(
|
||||||
|
'xpack.idxMgmt.deleteDataStreamsConfirmationModal.successDeleteMultipleNotificationMessageText',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'Deleted {numSuccesses, plural, one {# data stream} other {# data streams}}',
|
||||||
|
values: { numSuccesses: dataStreamsDeleted.length },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onClose({ hasDeletedDataStreams });
|
||||||
|
notificationService.showSuccessToast(successMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || (errors && errors.length)) {
|
||||||
|
const hasMultipleErrors =
|
||||||
|
(errors && errors.length > 1) || (error && dataStreams.length > 1);
|
||||||
|
|
||||||
|
const errorMessage = hasMultipleErrors
|
||||||
|
? i18n.translate(
|
||||||
|
'xpack.idxMgmt.deleteDataStreamsConfirmationModal.multipleErrorsNotificationMessageText',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Error deleting {count} data streams',
|
||||||
|
values: {
|
||||||
|
count: (errors && errors.length) || dataStreams.length,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: i18n.translate(
|
||||||
|
'xpack.idxMgmt.deleteDataStreamsConfirmationModal.errorNotificationMessageText',
|
||||||
|
{
|
||||||
|
defaultMessage: "Error deleting data stream '{name}'",
|
||||||
|
values: { name: (errors && errors[0].name) || dataStreams[0] },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
notificationService.showDangerToast(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiOverlayMask>
|
||||||
|
<EuiConfirmModal
|
||||||
|
buttonColor="danger"
|
||||||
|
data-test-subj="deleteDataStreamsConfirmation"
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.deleteDataStreamsConfirmationModal.modalTitleText"
|
||||||
|
defaultMessage="Delete {dataStreamsCount, plural, one {data stream} other {# data streams}}"
|
||||||
|
values={{ dataStreamsCount }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onCancel={() => onClose()}
|
||||||
|
onConfirm={handleDeleteDataStreams}
|
||||||
|
cancelButtonText={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.deleteDataStreamsConfirmationModal.cancelButtonLabel"
|
||||||
|
defaultMessage="Cancel"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
confirmButtonText={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.deleteDataStreamsConfirmationModal.confirmButtonLabel"
|
||||||
|
defaultMessage="Delete {dataStreamsCount, plural, one {data stream} other {data streams} }"
|
||||||
|
values={{ dataStreamsCount }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Fragment>
|
||||||
|
<EuiCallOut
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.deleteDataStreamsConfirmationModal.warningTitle"
|
||||||
|
defaultMessage="Deleting data streams also deletes indices"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
color="danger"
|
||||||
|
iconType="alert"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.deleteDataStreamsConfirmationModal.warningMessage"
|
||||||
|
defaultMessage="Data streams are collections of time series indices. Deleting a data stream will also delete its indices."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</EuiCallOut>
|
||||||
|
|
||||||
|
<EuiSpacer />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.deleteDataStreamsConfirmationModal.deleteDescription"
|
||||||
|
defaultMessage="You are about to delete {dataStreamsCount, plural, one {this data stream} other {these data streams} }:"
|
||||||
|
values={{ dataStreamsCount }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{dataStreams.map((name) => (
|
||||||
|
<li key={name}>{name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Fragment>
|
||||||
|
</EuiConfirmModal>
|
||||||
|
</EuiOverlayMask>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { DeleteDataStreamConfirmationModal } from './delete_data_stream_confirmation_modal';
|
|
@ -53,14 +53,21 @@ export function useLoadDataStreams() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement this API endpoint once we have content to surface in the detail panel.
|
|
||||||
export function useLoadDataStream(name: string) {
|
export function useLoadDataStream(name: string) {
|
||||||
return useRequest<DataStream[]>({
|
return useRequest<DataStream>({
|
||||||
path: `${API_BASE_PATH}/data_stream/${encodeURIComponent(name)}`,
|
path: `${API_BASE_PATH}/data_streams/${encodeURIComponent(name)}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteDataStreams(dataStreams: string[]) {
|
||||||
|
return sendRequest({
|
||||||
|
path: `${API_BASE_PATH}/delete_data_streams`,
|
||||||
|
method: 'post',
|
||||||
|
body: { dataStreams },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadIndices() {
|
export async function loadIndices() {
|
||||||
const response = await httpService.httpClient.get(`${API_BASE_PATH}/indices`);
|
const response = await httpService.httpClient.get(`${API_BASE_PATH}/indices`);
|
||||||
return response.data ? response.data : response;
|
return response.data ? response.data : response;
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { CoreSetup } from '../../../../src/core/public';
|
import { CoreSetup } from '../../../../src/core/public';
|
||||||
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
|
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
|
||||||
import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public';
|
import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public';
|
||||||
|
|
||||||
|
import { IngestManagerSetup } from '../../ingest_manager/public';
|
||||||
import { UIM_APP_NAME, PLUGIN } from '../common/constants';
|
import { UIM_APP_NAME, PLUGIN } from '../common/constants';
|
||||||
|
|
||||||
import { httpService } from './application/services/http';
|
import { httpService } from './application/services/http';
|
||||||
|
@ -25,6 +27,7 @@ export interface IndexManagementPluginSetup {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PluginsDependencies {
|
interface PluginsDependencies {
|
||||||
|
ingestManager?: IngestManagerSetup;
|
||||||
usageCollection: UsageCollectionSetup;
|
usageCollection: UsageCollectionSetup;
|
||||||
management: ManagementSetup;
|
management: ManagementSetup;
|
||||||
}
|
}
|
||||||
|
@ -42,7 +45,7 @@ export class IndexMgmtUIPlugin {
|
||||||
|
|
||||||
public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): IndexManagementPluginSetup {
|
public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): IndexManagementPluginSetup {
|
||||||
const { http, notifications } = coreSetup;
|
const { http, notifications } = coreSetup;
|
||||||
const { usageCollection, management } = plugins;
|
const { ingestManager, usageCollection, management } = plugins;
|
||||||
|
|
||||||
httpService.setup(http);
|
httpService.setup(http);
|
||||||
notificationService.setup(notifications);
|
notificationService.setup(notifications);
|
||||||
|
@ -60,7 +63,7 @@ export class IndexMgmtUIPlugin {
|
||||||
uiMetricService: this.uiMetricService,
|
uiMetricService: this.uiMetricService,
|
||||||
extensionsService: this.extensionsService,
|
extensionsService: this.extensionsService,
|
||||||
};
|
};
|
||||||
return mountManagementSection(coreSetup, usageCollection, services, params);
|
return mountManagementSection(coreSetup, usageCollection, services, params, ingestManager);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,20 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dataManagement.getDataStream = ca({
|
||||||
|
urls: [
|
||||||
|
{
|
||||||
|
fmt: '/_data_stream/<%=name%>',
|
||||||
|
req: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
// We don't allow the user to create a data stream in the UI or API. We're just adding this here
|
// We don't allow the user to create a data stream in the UI or API. We're just adding this here
|
||||||
// to enable the API integration tests.
|
// to enable the API integration tests.
|
||||||
dataManagement.createDataStream = ca({
|
dataManagement.createDataStream = ca({
|
||||||
|
|
|
@ -6,8 +6,11 @@
|
||||||
|
|
||||||
import { RouteDependencies } from '../../../types';
|
import { RouteDependencies } from '../../../types';
|
||||||
|
|
||||||
import { registerGetAllRoute } from './register_get_route';
|
import { registerGetOneRoute, registerGetAllRoute } from './register_get_route';
|
||||||
|
import { registerDeleteRoute } from './register_delete_route';
|
||||||
|
|
||||||
export function registerDataStreamRoutes(dependencies: RouteDependencies) {
|
export function registerDataStreamRoutes(dependencies: RouteDependencies) {
|
||||||
|
registerGetOneRoute(dependencies);
|
||||||
registerGetAllRoute(dependencies);
|
registerGetAllRoute(dependencies);
|
||||||
|
registerDeleteRoute(dependencies);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { schema, TypeOf } from '@kbn/config-schema';
|
||||||
|
|
||||||
|
import { RouteDependencies } from '../../../types';
|
||||||
|
import { addBasePath } from '../index';
|
||||||
|
import { wrapEsError } from '../../helpers';
|
||||||
|
|
||||||
|
const bodySchema = schema.object({
|
||||||
|
dataStreams: schema.arrayOf(schema.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function registerDeleteRoute({ router, license }: RouteDependencies) {
|
||||||
|
router.post(
|
||||||
|
{
|
||||||
|
path: addBasePath('/delete_data_streams'),
|
||||||
|
validate: { body: bodySchema },
|
||||||
|
},
|
||||||
|
license.guardApiRoute(async (ctx, req, res) => {
|
||||||
|
const { callAsCurrentUser } = ctx.dataManagement!.client;
|
||||||
|
const { dataStreams } = req.body as TypeOf<typeof bodySchema>;
|
||||||
|
|
||||||
|
const response: { dataStreamsDeleted: string[]; errors: any[] } = {
|
||||||
|
dataStreamsDeleted: [],
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
dataStreams.map(async (name: string) => {
|
||||||
|
try {
|
||||||
|
await callAsCurrentUser('dataManagement.deleteDataStream', {
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.dataStreamsDeleted.push(name);
|
||||||
|
} catch (e) {
|
||||||
|
return response.errors.push({
|
||||||
|
name,
|
||||||
|
error: wrapEsError(e),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.ok({ body: response });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,7 +4,9 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { deserializeDataStreamList } from '../../../../common/lib';
|
import { schema, TypeOf } from '@kbn/config-schema';
|
||||||
|
|
||||||
|
import { deserializeDataStream, deserializeDataStreamList } from '../../../../common/lib';
|
||||||
import { RouteDependencies } from '../../../types';
|
import { RouteDependencies } from '../../../types';
|
||||||
import { addBasePath } from '../index';
|
import { addBasePath } from '../index';
|
||||||
|
|
||||||
|
@ -32,3 +34,40 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerGetOneRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
|
||||||
|
const paramsSchema = schema.object({
|
||||||
|
name: schema.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
{
|
||||||
|
path: addBasePath('/data_streams/{name}'),
|
||||||
|
validate: { params: paramsSchema },
|
||||||
|
},
|
||||||
|
license.guardApiRoute(async (ctx, req, res) => {
|
||||||
|
const { name } = req.params as TypeOf<typeof paramsSchema>;
|
||||||
|
const { callAsCurrentUser } = ctx.dataManagement!.client;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataStream = await callAsCurrentUser('dataManagement.getDataStream', { name });
|
||||||
|
|
||||||
|
if (dataStream[0]) {
|
||||||
|
const body = deserializeDataStream(dataStream[0]);
|
||||||
|
return res.ok({ body });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.notFound();
|
||||||
|
} catch (e) {
|
||||||
|
if (isEsError(e)) {
|
||||||
|
return res.customError({
|
||||||
|
statusCode: e.statusCode,
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Case: default
|
||||||
|
return res.internalError({ body: e });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { PluginInitializerContext } from 'src/core/public';
|
import { PluginInitializerContext } from 'src/core/public';
|
||||||
import { IngestManagerPlugin } from './plugin';
|
import { IngestManagerPlugin } from './plugin';
|
||||||
|
|
||||||
export { IngestManagerStart } from './plugin';
|
export { IngestManagerSetup, IngestManagerStart } from './plugin';
|
||||||
|
|
||||||
export const plugin = (initializerContext: PluginInitializerContext) => {
|
export const plugin = (initializerContext: PluginInitializerContext) => {
|
||||||
return new IngestManagerPlugin(initializerContext);
|
return new IngestManagerPlugin(initializerContext);
|
||||||
|
|
|
@ -22,7 +22,11 @@ import { registerDatasource } from './applications/ingest_manager/sections/agent
|
||||||
|
|
||||||
export { IngestManagerConfigType } from '../common/types';
|
export { IngestManagerConfigType } from '../common/types';
|
||||||
|
|
||||||
export type IngestManagerSetup = void;
|
// We need to provide an object instead of void so that dependent plugins know when Ingest Manager
|
||||||
|
// is disabled.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface IngestManagerSetup {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes public IngestManager plugin contract returned at the `start` stage.
|
* Describes public IngestManager plugin contract returned at the `start` stage.
|
||||||
*/
|
*/
|
||||||
|
@ -72,6 +76,8 @@ export class IngestManagerPlugin
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(core: CoreStart): Promise<IngestManagerStart> {
|
public async start(core: CoreStart): Promise<IngestManagerStart> {
|
||||||
|
|
Loading…
Reference in a new issue