Add delete data stream action and detail panel (#68919)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
CJ Cenizal 2020-06-24 19:30:12 -07:00 committed by GitHub
parent bcc62095f0
commit b48c8bf355
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 860 additions and 183 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,8 @@
], ],
"optionalPlugins": [ "optionalPlugins": [
"security", "security",
"usageCollection" "usageCollection",
"ingestManager"
], ],
"configPath": ["xpack", "index_management"] "configPath": ["xpack", "index_management"]
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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({

View file

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

View file

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

View file

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

View file

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

View file

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