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 = []) => {
|
||||
server.respondWith('POST', `${API_BASE_PATH}/delete_index_templates`, [
|
||||
200,
|
||||
|
@ -80,6 +96,8 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
|
|||
setLoadTemplatesResponse,
|
||||
setLoadIndicesResponse,
|
||||
setLoadDataStreamsResponse,
|
||||
setLoadDataStreamResponse,
|
||||
setDeleteDataStreamResponse,
|
||||
setDeleteTemplateResponse,
|
||||
setLoadTemplateResponse,
|
||||
setCreateTemplateResponse,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React from 'react';
|
||||
import axios from 'axios';
|
||||
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
import {
|
||||
notificationServiceMock,
|
||||
|
@ -33,7 +34,7 @@ export const services = {
|
|||
services.uiMetricService.setup({ reportUiStats() {} } as any);
|
||||
setExtensionsService(services.extensionsService);
|
||||
setUiMetricService(services.uiMetricService);
|
||||
const appDependencies = { services, core: {}, plugins: {} } as any;
|
||||
const appDependencies = { services, core: { getUrlForApp: () => {} }, plugins: {} } as any;
|
||||
|
||||
export const setupEnvironment = () => {
|
||||
// Mock initialization of services
|
||||
|
@ -51,8 +52,13 @@ export const setupEnvironment = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const WithAppDependencies = (Comp: any) => (props: any) => (
|
||||
<AppContextProvider value={appDependencies}>
|
||||
<Comp {...props} />
|
||||
</AppContextProvider>
|
||||
);
|
||||
export const WithAppDependencies = (Comp: any, overridingDependencies: any = {}) => (
|
||||
props: any
|
||||
) => {
|
||||
const mergedDependencies = merge({}, appDependencies, overridingDependencies);
|
||||
return (
|
||||
<AppContextProvider value={mergedDependencies}>
|
||||
<Comp {...props} />
|
||||
</AppContextProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
|
||||
import {
|
||||
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 { 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> {
|
||||
actions: {
|
||||
goToDataStreamsList: () => void;
|
||||
clickEmptyPromptIndexTemplateLink: () => void;
|
||||
clickReloadButton: () => void;
|
||||
clickNameAt: (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();
|
||||
|
||||
/**
|
||||
|
@ -48,15 +60,17 @@ export const setup = async (): Promise<DataStreamsTabTestBed> => {
|
|||
testBed.find('data_streamsTab').simulate('click');
|
||||
};
|
||||
|
||||
const clickEmptyPromptIndexTemplateLink = async () => {
|
||||
const { find, component, router } = testBed;
|
||||
|
||||
const findEmptyPromptIndexTemplateLink = () => {
|
||||
const { find } = testBed;
|
||||
const templateLink = find('dataStreamsEmptyPromptTemplateLink');
|
||||
return templateLink;
|
||||
};
|
||||
|
||||
const clickEmptyPromptIndexTemplateLink = async () => {
|
||||
const { component, router } = testBed;
|
||||
await act(async () => {
|
||||
router.navigateTo(templateLink.props().href!);
|
||||
router.navigateTo(findEmptyPromptIndexTemplateLink().props().href!);
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
|
@ -65,10 +79,15 @@ export const setup = async (): Promise<DataStreamsTabTestBed> => {
|
|||
find('reloadButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickIndicesAt = async (index: number) => {
|
||||
const { component, table, router } = testBed;
|
||||
const findTestSubjectAt = (testSubject: string, index: number) => {
|
||||
const { table } = testBed;
|
||||
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 () => {
|
||||
router.navigateTo(indicesLink.props().href!);
|
||||
|
@ -77,14 +96,71 @@ export const setup = async (): Promise<DataStreamsTabTestBed> => {
|
|||
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 {
|
||||
...testBed,
|
||||
actions: {
|
||||
goToDataStreamsList,
|
||||
clickEmptyPromptIndexTemplateLink,
|
||||
clickReloadButton,
|
||||
clickNameAt,
|
||||
clickIndicesAt,
|
||||
clickDeletActionAt,
|
||||
clickConfirmDelete,
|
||||
clickDeletDataStreamButton,
|
||||
},
|
||||
findDeleteActionAt,
|
||||
findDeleteConfirmationModal,
|
||||
findDetailPanel,
|
||||
findDetailPanelTitle,
|
||||
findEmptyPromptIndexTemplateLink,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -19,61 +19,38 @@ describe('Data Streams tab', () => {
|
|||
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', () => {
|
||||
beforeEach(async () => {
|
||||
const { actions, component } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([]);
|
||||
httpRequestsMockHelpers.setLoadDataStreamsResponse([]);
|
||||
httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] });
|
||||
|
||||
await act(async () => {
|
||||
actions.goToDataStreamsList();
|
||||
});
|
||||
|
||||
component.update();
|
||||
});
|
||||
|
||||
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('emptyPrompt')).toBe(true);
|
||||
});
|
||||
|
||||
test('goes to index templates tab when "Get started" link is clicked', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
test('when Ingest Manager is disabled, goes to index templates tab when "Get started" link is clicked', async () => {
|
||||
testBed = await setup({
|
||||
plugins: {},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
testBed.actions.goToDataStreamsList();
|
||||
});
|
||||
|
||||
const { actions, exists, component } = testBed;
|
||||
component.update();
|
||||
|
||||
await act(async () => {
|
||||
actions.clickEmptyPromptIndexTemplateLink();
|
||||
|
@ -81,32 +58,77 @@ describe('Data Streams tab', () => {
|
|||
|
||||
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', () => {
|
||||
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([
|
||||
createDataStreamPayload('dataStream1'),
|
||||
dataStreamForDetailPanel,
|
||||
createDataStreamPayload('dataStream2'),
|
||||
]);
|
||||
|
||||
httpRequestsMockHelpers.setLoadDataStreamResponse(dataStreamForDetailPanel);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
await act(async () => {
|
||||
actions.goToDataStreamsList();
|
||||
testBed.actions.goToDataStreamsList();
|
||||
});
|
||||
|
||||
component.update();
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
test('lists them in the table', async () => {
|
||||
const { table } = testBed;
|
||||
|
||||
const { tableCellsValues } = table.getMetaData('dataStreamTable');
|
||||
|
||||
expect(tableCellsValues).toEqual([
|
||||
['dataStream1', '1', '@timestamp', '1'],
|
||||
['dataStream2', '1', '@timestamp', '1'],
|
||||
['', 'dataStream1', '1', ''],
|
||||
['', 'dataStream2', '1', ''],
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -126,12 +148,90 @@ describe('Data Streams tab', () => {
|
|||
|
||||
test('clicking the indices count navigates to the backing indices', async () => {
|
||||
const { table, actions } = testBed;
|
||||
|
||||
await actions.clickIndicesAt(0);
|
||||
|
||||
expect(table.getMetaData('indexTable').tableCellsValues).toEqual([
|
||||
['', '', '', '', '', '', '', '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 { ReactWrapper } from 'enzyme';
|
||||
|
||||
import {
|
||||
registerTestBed,
|
||||
|
@ -34,6 +35,8 @@ export interface IndicesTestBed extends TestBed<TestSubjects> {
|
|||
clickIncludeHiddenIndicesToggle: () => void;
|
||||
clickDataStreamAt: (index: number) => void;
|
||||
};
|
||||
findDataStreamDetailPanel: () => ReactWrapper;
|
||||
findDataStreamDetailPanelTitle: () => string;
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<IndicesTestBed> => {
|
||||
|
@ -77,6 +80,16 @@ export const setup = async (): Promise<IndicesTestBed> => {
|
|||
component.update();
|
||||
};
|
||||
|
||||
const findDataStreamDetailPanel = () => {
|
||||
const { find } = testBed;
|
||||
return find('dataStreamDetailPanel');
|
||||
};
|
||||
|
||||
const findDataStreamDetailPanelTitle = () => {
|
||||
const { find } = testBed;
|
||||
return find('dataStreamDetailPanelTitle').text();
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
|
@ -85,5 +98,7 @@ export const setup = async (): Promise<IndicesTestBed> => {
|
|||
clickIncludeHiddenIndicesToggle,
|
||||
clickDataStreamAt,
|
||||
},
|
||||
findDataStreamDetailPanel,
|
||||
findDataStreamDetailPanelTitle,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -70,10 +70,10 @@ describe('<IndexManagementHome />', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
httpRequestsMockHelpers.setLoadDataStreamsResponse([
|
||||
createDataStreamPayload('dataStream1'),
|
||||
createDataStreamPayload('dataStream2'),
|
||||
]);
|
||||
// The detail panel should still appear even if there are no data streams.
|
||||
httpRequestsMockHelpers.setLoadDataStreamsResponse([]);
|
||||
|
||||
httpRequestsMockHelpers.setLoadDataStreamResponse(createDataStreamPayload('dataStream1'));
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
|
@ -86,13 +86,16 @@ describe('<IndexManagementHome />', () => {
|
|||
});
|
||||
|
||||
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([
|
||||
['dataStream1', '1', '@timestamp', '1'],
|
||||
]);
|
||||
expect(findDataStreamDetailPanel().length).toBe(1);
|
||||
expect(findDataStreamDetailPanelTitle()).toBe('dataStream1');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
|
||||
import { DataStream, DataStreamFromEs } from '../types';
|
||||
|
||||
export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]): DataStream[] {
|
||||
return dataStreamsFromEs.map(({ name, timestamp_field, indices, generation }) => ({
|
||||
export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataStream {
|
||||
const { name, timestamp_field, indices, generation } = dataStreamFromEs;
|
||||
|
||||
return {
|
||||
name,
|
||||
timeStampField: timestamp_field,
|
||||
indices: indices.map(
|
||||
|
@ -17,5 +19,9 @@ export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[])
|
|||
})
|
||||
),
|
||||
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.
|
||||
*/
|
||||
|
||||
export { deserializeDataStreamList } from './data_stream_serialization';
|
||||
export { deserializeDataStream, deserializeDataStreamList } from './data_stream_serialization';
|
||||
|
||||
export {
|
||||
deserializeLegacyTemplateList,
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
],
|
||||
"optionalPlugins": [
|
||||
"security",
|
||||
"usageCollection"
|
||||
"usageCollection",
|
||||
"ingestManager"
|
||||
],
|
||||
"configPath": ["xpack", "index_management"]
|
||||
}
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
|
||||
import React, { createContext, useContext } from 'react';
|
||||
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 { UiMetricService, NotificationService, HttpService } from './services';
|
||||
import { ExtensionsService } from '../services';
|
||||
|
@ -22,6 +23,7 @@ export interface AppDependencies {
|
|||
};
|
||||
plugins: {
|
||||
usageCollection: UsageCollectionSetup;
|
||||
ingestManager?: IngestManagerSetup;
|
||||
};
|
||||
services: {
|
||||
uiMetricService: UiMetricService<IndexMgmtMetricsType>;
|
||||
|
|
|
@ -8,6 +8,7 @@ import { CoreSetup } from 'src/core/public';
|
|||
import { ManagementAppMountParams } from 'src/plugins/management/public/';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
|
||||
import { IngestManagerSetup } from '../../../ingest_manager/public';
|
||||
import { ExtensionsService } from '../services';
|
||||
import { IndexMgmtMetricsType } from '../types';
|
||||
import { AppDependencies } from './app_context';
|
||||
|
@ -28,7 +29,8 @@ export async function mountManagementSection(
|
|||
coreSetup: CoreSetup,
|
||||
usageCollection: UsageCollectionSetup,
|
||||
services: InternalServices,
|
||||
params: ManagementAppMountParams
|
||||
params: ManagementAppMountParams,
|
||||
ingestManager?: IngestManagerSetup
|
||||
) {
|
||||
const { element, setBreadcrumbs, history } = params;
|
||||
const [core] = await coreSetup.getStartServices();
|
||||
|
@ -44,6 +46,7 @@ export async function mountManagementSection(
|
|||
},
|
||||
plugins: {
|
||||
usageCollection,
|
||||
ingestManager,
|
||||
},
|
||||
services,
|
||||
history,
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
* 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 {
|
||||
EuiButton,
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
|
@ -15,14 +16,18 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { SectionLoading, SectionError, Error } from '../../../../components';
|
||||
import { useLoadDataStream } from '../../../../services/api';
|
||||
import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
|
||||
|
||||
interface Props {
|
||||
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 [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||
|
||||
let content;
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -61,44 +68,97 @@ export const DataStreamDetailPanel: React.FunctionComponent<Props> = ({
|
|||
/>
|
||||
);
|
||||
} 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 (
|
||||
<EuiFlyout
|
||||
onClose={onClose}
|
||||
data-test-subj="dataStreamDetailPanel"
|
||||
aria-labelledby="dataStreamDetailPanelTitle"
|
||||
size="m"
|
||||
maxWidth={500}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="dataStreamDetailPanelTitle" data-test-subj="title">
|
||||
{dataStreamName}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<>
|
||||
{isDeleting ? (
|
||||
<DeleteDataStreamConfirmationModal
|
||||
onClose={(data) => {
|
||||
if (data && data.hasDeletedDataStreams) {
|
||||
onClose(true);
|
||||
} else {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}}
|
||||
dataStreams={[dataStreamName]}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<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>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={onClose}
|
||||
data-test-subj="closeDetailsButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamDetailPanel.closeButtonLabel"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
<EuiFlyoutBody data-test-subj="content">{content}</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={() => onClose()}
|
||||
data-test-subj="closeDetailsButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.dataStreamDetailPanel.closeButtonLabel"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
{!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 { reactRouterNavigate } from '../../../../shared_imports';
|
||||
import { useAppContext } from '../../../app_context';
|
||||
import { SectionError, SectionLoading, Error } from '../../../components';
|
||||
import { useLoadDataStreams } from '../../../services/api';
|
||||
import { decodePathFromReactRouter } from '../../../services/routing';
|
||||
import { Section } from '../../home';
|
||||
import { DataStreamTable } from './data_stream_table';
|
||||
import { DataStreamDetailPanel } from './data_stream_detail_panel';
|
||||
|
||||
interface MatchParams {
|
||||
dataStreamName?: string;
|
||||
|
@ -26,6 +30,11 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
},
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: { getUrlForApp },
|
||||
plugins: { ingestManager },
|
||||
} = useAppContext();
|
||||
|
||||
const { error, isLoading, data: dataStreams, sendRequest: reload } = useLoadDataStreams();
|
||||
|
||||
let content;
|
||||
|
@ -67,22 +76,52 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
<p>
|
||||
<FormattedMessage
|
||||
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}."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
data-test-subj="dataStreamsEmptyPromptTemplateLink"
|
||||
{...reactRouterNavigate(history, {
|
||||
pathname: '/templates',
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.idxMgmt.dataStreamList.emptyPrompt.getStartedLink', {
|
||||
defaultMessage: 'composable index template',
|
||||
})}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
defaultMessage="Data streams represent collections of time series indices."
|
||||
/>
|
||||
{' ' /* 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>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
|
@ -104,24 +143,38 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
<EuiSpacer size="l" />
|
||||
|
||||
<DataStreamTable
|
||||
filters={dataStreamName !== undefined ? `name=${dataStreamName}` : ''}
|
||||
filters={
|
||||
dataStreamName !== undefined ? `name=${decodePathFromReactRouter(dataStreamName)}` : ''
|
||||
}
|
||||
dataStreams={dataStreams}
|
||||
reload={reload}
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink } from '@elastic/eui';
|
||||
|
@ -13,6 +13,8 @@ import { ScopedHistory } from 'kibana/public';
|
|||
import { DataStream } from '../../../../../../common/types';
|
||||
import { reactRouterNavigate } from '../../../../../shared_imports';
|
||||
import { encodePathForReactRouter } from '../../../../services/routing';
|
||||
import { Section } from '../../../home';
|
||||
import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
|
||||
|
||||
interface Props {
|
||||
dataStreams?: DataStream[];
|
||||
|
@ -27,6 +29,9 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
|||
history,
|
||||
filters,
|
||||
}) => {
|
||||
const [selection, setSelection] = useState<DataStream[]>([]);
|
||||
const [dataStreamsToDelete, setDataStreamsToDelete] = useState<string[]>([]);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<DataStream>> = [
|
||||
{
|
||||
field: 'name',
|
||||
|
@ -35,7 +40,19 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
|||
}),
|
||||
truncateText: 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',
|
||||
|
@ -59,20 +76,27 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
|||
),
|
||||
},
|
||||
{
|
||||
field: 'timeStampField.name',
|
||||
name: i18n.translate('xpack.idxMgmt.dataStreamList.table.timeStampFieldColumnTitle', {
|
||||
defaultMessage: 'Timestamp field',
|
||||
name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionColumnTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'generation',
|
||||
name: i18n.translate('xpack.idxMgmt.dataStreamList.table.generationFieldColumnTitle', {
|
||||
defaultMessage: 'Generation',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
actions: [
|
||||
{
|
||||
name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteText', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
description: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteDecription', {
|
||||
defaultMessage: 'Delete this data stream',
|
||||
}),
|
||||
icon: 'trash',
|
||||
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;
|
||||
|
||||
const selectionConfig = {
|
||||
onSelectionChange: setSelection,
|
||||
};
|
||||
|
||||
const searchConfig = {
|
||||
query: filters,
|
||||
box: {
|
||||
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: [
|
||||
<EuiButton
|
||||
color="secondary"
|
||||
|
@ -112,6 +153,18 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
{dataStreamsToDelete && dataStreamsToDelete.length > 0 ? (
|
||||
<DeleteDataStreamConfirmationModal
|
||||
onClose={(data) => {
|
||||
if (data && data.hasDeletedDataStreams) {
|
||||
reload();
|
||||
} else {
|
||||
setDataStreamsToDelete([]);
|
||||
}
|
||||
}}
|
||||
dataStreams={dataStreamsToDelete}
|
||||
/>
|
||||
) : null}
|
||||
<EuiInMemoryTable
|
||||
items={dataStreams || []}
|
||||
itemId="name"
|
||||
|
@ -119,6 +172,7 @@ export const DataStreamTable: React.FunctionComponent<Props> = ({
|
|||
search={searchConfig}
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
selection={selectionConfig}
|
||||
pagination={pagination}
|
||||
rowProps={() => ({
|
||||
'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) {
|
||||
return useRequest<DataStream[]>({
|
||||
path: `${API_BASE_PATH}/data_stream/${encodeURIComponent(name)}`,
|
||||
return useRequest<DataStream>({
|
||||
path: `${API_BASE_PATH}/data_streams/${encodeURIComponent(name)}`,
|
||||
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() {
|
||||
const response = await httpService.httpClient.get(`${API_BASE_PATH}/indices`);
|
||||
return response.data ? response.data : response;
|
||||
|
|
|
@ -8,6 +8,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { CoreSetup } from '../../../../src/core/public';
|
||||
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
|
||||
import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public';
|
||||
|
||||
import { IngestManagerSetup } from '../../ingest_manager/public';
|
||||
import { UIM_APP_NAME, PLUGIN } from '../common/constants';
|
||||
|
||||
import { httpService } from './application/services/http';
|
||||
|
@ -25,6 +27,7 @@ export interface IndexManagementPluginSetup {
|
|||
}
|
||||
|
||||
interface PluginsDependencies {
|
||||
ingestManager?: IngestManagerSetup;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
management: ManagementSetup;
|
||||
}
|
||||
|
@ -42,7 +45,7 @@ export class IndexMgmtUIPlugin {
|
|||
|
||||
public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): IndexManagementPluginSetup {
|
||||
const { http, notifications } = coreSetup;
|
||||
const { usageCollection, management } = plugins;
|
||||
const { ingestManager, usageCollection, management } = plugins;
|
||||
|
||||
httpService.setup(http);
|
||||
notificationService.setup(notifications);
|
||||
|
@ -60,7 +63,7 @@ export class IndexMgmtUIPlugin {
|
|||
uiMetricService: this.uiMetricService,
|
||||
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',
|
||||
});
|
||||
|
||||
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
|
||||
// to enable the API integration tests.
|
||||
dataManagement.createDataStream = ca({
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
|
||||
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) {
|
||||
registerGetOneRoute(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.
|
||||
*/
|
||||
|
||||
import { deserializeDataStreamList } from '../../../../common/lib';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import { deserializeDataStream, deserializeDataStreamList } from '../../../../common/lib';
|
||||
import { RouteDependencies } from '../../../types';
|
||||
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 { IngestManagerPlugin } from './plugin';
|
||||
|
||||
export { IngestManagerStart } from './plugin';
|
||||
export { IngestManagerSetup, IngestManagerStart } from './plugin';
|
||||
|
||||
export const plugin = (initializerContext: PluginInitializerContext) => {
|
||||
return new IngestManagerPlugin(initializerContext);
|
||||
|
|
|
@ -22,7 +22,11 @@ import { registerDatasource } from './applications/ingest_manager/sections/agent
|
|||
|
||||
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.
|
||||
*/
|
||||
|
@ -72,6 +76,8 @@ export class IngestManagerPlugin
|
|||
};
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public async start(core: CoreStart): Promise<IngestManagerStart> {
|
||||
|
|
Loading…
Reference in a new issue