diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index 75d1b69eb6157..56d76da522ac2 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -27,6 +27,14 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadDataStreamsResponse = (response: HttpResponse = []) => { + server.respondWith('GET', `${API_BASE_PATH}/data_streams`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + const setDeleteTemplateResponse = (response: HttpResponse = []) => { server.respondWith('POST', `${API_BASE_PATH}/delete_index_templates`, [ 200, @@ -71,6 +79,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { return { setLoadTemplatesResponse, setLoadIndicesResponse, + setLoadDataStreamsResponse, setDeleteTemplateResponse, setLoadTemplateResponse, setCreateTemplateResponse, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts index 8e7755a65af3c..f581083e28cc6 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts @@ -10,44 +10,4 @@ export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../.. export { setupEnvironment, WithAppDependencies, services } from './setup_environment'; -export type TestSubjects = - | 'aliasesTab' - | 'appTitle' - | 'cell' - | 'closeDetailsButton' - | 'createTemplateButton' - | 'createLegacyTemplateButton' - | 'deleteSystemTemplateCallOut' - | 'deleteTemplateButton' - | 'deleteTemplatesConfirmation' - | 'documentationLink' - | 'emptyPrompt' - | 'manageTemplateButton' - | 'mappingsTab' - | 'noAliasesCallout' - | 'noMappingsCallout' - | 'noSettingsCallout' - | 'indicesList' - | 'indicesTab' - | 'indexTableIncludeHiddenIndicesToggle' - | 'indexTableIndexNameLink' - | 'reloadButton' - | 'reloadIndicesButton' - | 'row' - | 'sectionError' - | 'sectionLoading' - | 'settingsTab' - | 'summaryTab' - | 'summaryTitle' - | 'systemTemplatesSwitch' - | 'templateDetails' - | 'templateDetails.manageTemplateButton' - | 'templateDetails.sectionLoading' - | 'templateDetails.tab' - | 'templateDetails.title' - | 'templateList' - | 'templateTable' - | 'templatesTab' - | 'legacyTemplateTable' - | 'viewButton' - | 'filterList.filterItem'; +export { TestSubjects } from './test_subjects'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts new file mode 100644 index 0000000000000..4e297118b0fdd --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -0,0 +1,51 @@ +/* + * 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 type TestSubjects = + | 'aliasesTab' + | 'appTitle' + | 'cell' + | 'closeDetailsButton' + | 'createLegacyTemplateButton' + | 'createTemplateButton' + | 'dataStreamsEmptyPromptTemplateLink' + | 'dataStreamTable' + | 'dataStreamTable' + | 'deleteSystemTemplateCallOut' + | 'deleteTemplateButton' + | 'deleteTemplatesConfirmation' + | 'documentationLink' + | 'emptyPrompt' + | 'filterList.filterItem' + | 'indexTable' + | 'indexTableIncludeHiddenIndicesToggle' + | 'indexTableIndexNameLink' + | 'indicesList' + | 'indicesTab' + | 'legacyTemplateTable' + | 'manageTemplateButton' + | 'mappingsTab' + | 'noAliasesCallout' + | 'noMappingsCallout' + | 'noSettingsCallout' + | 'reloadButton' + | 'reloadIndicesButton' + | 'row' + | 'sectionError' + | 'sectionLoading' + | 'settingsTab' + | 'summaryTab' + | 'summaryTitle' + | 'systemTemplatesSwitch' + | 'templateDetails' + | 'templateDetails.manageTemplateButton' + | 'templateDetails.sectionLoading' + | 'templateDetails.tab' + | 'templateDetails.title' + | 'templateList' + | 'templatesTab' + | 'templateTable' + | 'viewButton'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts new file mode 100644 index 0000000000000..ef6aca44a1754 --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -0,0 +1,101 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { + registerTestBed, + TestBed, + TestBedConfig, + findTestSubject, +} from '../../../../../test_utils'; +import { DataStream } from '../../../common'; +import { IndexManagementHome } from '../../../public/application/sections/home'; // 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'; + +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 { + actions: { + goToDataStreamsList: () => void; + clickEmptyPromptIndexTemplateLink: () => void; + clickReloadButton: () => void; + clickIndicesAt: (index: number) => void; + }; +} + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + /** + * User Actions + */ + + const goToDataStreamsList = () => { + testBed.find('data_streamsTab').simulate('click'); + }; + + const clickEmptyPromptIndexTemplateLink = async () => { + const { find, component, router } = testBed; + + const templateLink = find('dataStreamsEmptyPromptTemplateLink'); + + await act(async () => { + router.navigateTo(templateLink.props().href!); + }); + + component.update(); + }; + + const clickReloadButton = () => { + const { find } = testBed; + find('reloadButton').simulate('click'); + }; + + const clickIndicesAt = async (index: number) => { + const { component, table, router } = testBed; + const { rows } = table.getMetaData('dataStreamTable'); + const indicesLink = findTestSubject(rows[index].reactWrapper, 'indicesLink'); + + await act(async () => { + router.navigateTo(indicesLink.props().href!); + }); + + component.update(); + }; + + return { + ...testBed, + actions: { + goToDataStreamsList, + clickEmptyPromptIndexTemplateLink, + clickReloadButton, + clickIndicesAt, + }, + }; +}; + +export const createDataStreamPayload = (name: string): DataStream => ({ + name, + timeStampField: '@timestamp', + indices: [ + { + name: 'indexName', + uuid: 'indexId', + }, + ], + generation: 1, +}); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts new file mode 100644 index 0000000000000..efe2e2d0c74ae --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { setupEnvironment } from '../helpers'; + +import { DataStreamsTabTestBed, setup, createDataStreamPayload } from './data_streams_tab.helpers'; + +describe('Data Streams tab', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: DataStreamsTabTestBed; + + afterAll(() => { + 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.setLoadDataStreamsResponse([]); + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); + + await act(async () => { + actions.goToDataStreamsList(); + }); + + component.update(); + }); + + test('displays an empty prompt', async () => { + const { exists } = testBed; + + 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; + + await act(async () => { + actions.clickEmptyPromptIndexTemplateLink(); + }); + + expect(exists('templateList')).toBe(true); + }); + }); + + describe('when there are data streams', () => { + beforeEach(async () => { + const { actions, component } = testBed; + + httpRequestsMockHelpers.setLoadDataStreamsResponse([ + createDataStreamPayload('dataStream1'), + createDataStreamPayload('dataStream2'), + ]); + + await act(async () => { + actions.goToDataStreamsList(); + }); + + 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'], + ]); + }); + + test('has a button to reload the data streams', async () => { + const { exists, actions } = testBed; + const totalRequests = server.requests.length; + + expect(exists('reloadButton')).toBe(true); + + await act(async () => { + actions.clickReloadButton(); + }); + + expect(server.requests.length).toBe(totalRequests + 1); + expect(server.requests[server.requests.length - 1].url).toBe(`${API_BASE_PATH}/data_streams`); + }); + + 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'], + ]); + }); + }); +}); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts index e995932dfa00d..f00348aacbf08 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts @@ -6,8 +6,13 @@ import { act } from 'react-dom/test-utils'; -import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; -import { IndexList } from '../../../public/application/sections/home/index_list'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { + registerTestBed, + TestBed, + TestBedConfig, + findTestSubject, +} from '../../../../../test_utils'; +import { IndexManagementHome } from '../../../public/application/sections/home'; // 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'; @@ -15,18 +20,19 @@ const testBedConfig: TestBedConfig = { store: () => indexManagementStore(services as any), memoryRouter: { initialEntries: [`/indices?includeHiddenIndices=true`], - componentRoutePath: `/:section(indices|templates)`, + componentRoutePath: `/:section(indices|data_streams)`, }, doMountAsync: true, }; -const initTestBed = registerTestBed(WithAppDependencies(IndexList), testBedConfig); +const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig); export interface IndicesTestBed extends TestBed { actions: { selectIndexDetailsTab: (tab: 'settings' | 'mappings' | 'stats' | 'edit_settings') => void; getIncludeHiddenIndicesToggleStatus: () => boolean; clickIncludeHiddenIndicesToggle: () => void; + clickDataStreamAt: (index: number) => void; }; } @@ -59,12 +65,25 @@ export const setup = async (): Promise => { component.update(); }; + const clickDataStreamAt = async (index: number) => { + const { component, table, router } = testBed; + const { rows } = table.getMetaData('indexTable'); + const dataStreamLink = findTestSubject(rows[index].reactWrapper, 'dataStreamLink'); + + await act(async () => { + router.navigateTo(dataStreamLink.props().href!); + }); + + component.update(); + }; + return { ...testBed, actions: { selectIndexDetailsTab, getIncludeHiddenIndicesToggleStatus, clickIncludeHiddenIndicesToggle, + clickDataStreamAt, }, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index 11c25ffbb590f..c2d955bb4dfce 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -9,6 +9,7 @@ import { act } from 'react-dom/test-utils'; import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment, nextTick } from '../helpers'; import { IndicesTestBed, setup } from './indices_tab.helpers'; +import { createDataStreamPayload } from './data_streams_tab.helpers'; /** * The below import is required to avoid a console error warn from the "brace" package @@ -52,6 +53,49 @@ describe('', () => { }); }); + describe('data stream column', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([ + { + health: '', + status: '', + primary: '', + replica: '', + documents: '', + documents_deleted: '', + size: '', + primary_size: '', + name: 'data-stream-index', + data_stream: 'dataStream1', + }, + ]); + + httpRequestsMockHelpers.setLoadDataStreamsResponse([ + createDataStreamPayload('dataStream1'), + createDataStreamPayload('dataStream2'), + ]); + + testBed = await setup(); + + await act(async () => { + const { component } = testBed; + + await nextTick(); + component.update(); + }); + }); + + test('navigates to the data stream in the Data Streams tab', async () => { + const { table, actions } = testBed; + + await actions.clickDataStreamAt(0); + + expect(table.getMetaData('dataStreamTable').tableCellsValues).toEqual([ + ['dataStream1', '1', '@timestamp', '1'], + ]); + }); + }); + describe('index detail panel with % character in index name', () => { const indexName = 'test%'; beforeEach(async () => { diff --git a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts new file mode 100644 index 0000000000000..9d267210a6b31 --- /dev/null +++ b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts @@ -0,0 +1,21 @@ +/* + * 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 { DataStream, DataStreamFromEs } from '../types'; + +export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]): DataStream[] { + return dataStreamsFromEs.map(({ name, timestamp_field, indices, generation }) => ({ + name, + timeStampField: timestamp_field, + indices: indices.map( + ({ index_name, index_uuid }: { index_name: string; index_uuid: string }) => ({ + name: index_name, + uuid: index_uuid, + }) + ), + generation, + })); +} diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index c67d28da2c24b..fce4d8ccc2502 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.ts @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +export { deserializeDataStreamList } from './data_stream_serialization'; + export { deserializeLegacyTemplateList, deserializeTemplateList, diff --git a/x-pack/plugins/index_management/common/types/data_streams.ts b/x-pack/plugins/index_management/common/types/data_streams.ts new file mode 100644 index 0000000000000..5b743296d868b --- /dev/null +++ b/x-pack/plugins/index_management/common/types/data_streams.ts @@ -0,0 +1,29 @@ +/* + * 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 interface DataStreamFromEs { + name: string; + timestamp_field: string; + indices: DataStreamIndexFromEs[]; + generation: number; +} + +export interface DataStreamIndexFromEs { + index_name: string; + index_uuid: string; +} + +export interface DataStream { + name: string; + timeStampField: string; + indices: DataStreamIndex[]; + generation: number; +} + +export interface DataStreamIndex { + name: string; + uuid: string; +} diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts index 81a06156dd291..c4ba60573d430 100644 --- a/x-pack/plugins/index_management/common/types/index.ts +++ b/x-pack/plugins/index_management/common/types/index.ts @@ -12,4 +12,6 @@ export * from './mappings'; export * from './templates'; +export { DataStreamFromEs, DataStream, DataStreamIndex } from './data_streams'; + export * from './component_templates'; diff --git a/x-pack/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx index bfd99de6949e5..92197bee30c88 100644 --- a/x-pack/plugins/index_management/public/application/app.tsx +++ b/x-pack/plugins/index_management/public/application/app.tsx @@ -31,9 +31,9 @@ export const App = ({ history }: { history: ScopedHistory }) => { // Export this so we can test it with a different router. export const AppWithoutRouter = () => ( - - - + + + diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx new file mode 100644 index 0000000000000..a6c8b83a05f98 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -0,0 +1,104 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { SectionLoading, SectionError, Error } from '../../../../components'; +import { useLoadDataStream } from '../../../../services/api'; + +interface Props { + dataStreamName: string; + onClose: () => void; +} + +/** + * NOTE: This currently isn't in use by data_stream_list.tsx because it doesn't contain any + * information that doesn't already exist in the table. We'll use it once we add additional + * info, e.g. storage size, docs count. + */ +export const DataStreamDetailPanel: React.FunctionComponent = ({ + dataStreamName, + onClose, +}) => { + const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error as Error} + data-test-subj="sectionError" + /> + ); + } else if (dataStream) { + content = {JSON.stringify(dataStream)}; + } + + return ( + + + +

+ {dataStreamName} +

+
+
+ + {content} + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts new file mode 100644 index 0000000000000..3f45267c032ed --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts @@ -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 { DataStreamDetailPanel } from './data_stream_detail_panel'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx new file mode 100644 index 0000000000000..951c4a0d7f3c3 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -0,0 +1,127 @@ +/* + * 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 from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; + +import { reactRouterNavigate } from '../../../../shared_imports'; +import { SectionError, SectionLoading, Error } from '../../../components'; +import { useLoadDataStreams } from '../../../services/api'; +import { DataStreamTable } from './data_stream_table'; + +interface MatchParams { + dataStreamName?: string; +} + +export const DataStreamList: React.FunctionComponent> = ({ + match: { + params: { dataStreamName }, + }, + history, +}) => { + const { error, isLoading, data: dataStreams, sendRequest: reload } = useLoadDataStreams(); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error as Error} + /> + ); + } else if (Array.isArray(dataStreams) && dataStreams.length === 0) { + content = ( + + + + } + body={ +

+ + {i18n.translate('xpack.idxMgmt.dataStreamList.emptyPrompt.getStartedLink', { + defaultMessage: 'composable index template', + })} + + ), + }} + /> +

+ } + data-test-subj="emptyPrompt" + /> + ); + } else if (Array.isArray(dataStreams) && dataStreams.length > 0) { + content = ( + <> + {/* TODO: Add a switch for toggling on data streams created by Ingest Manager */} + + + + + + + + + + + {/* TODO: Implement this once we have something to put in here, e.g. storage size, docs count */} + {/* dataStreamName && ( + { + history.push('/data_streams'); + }} + /> + )*/} + + ); + } + + return
{content}
; +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx new file mode 100644 index 0000000000000..54b215e561b46 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx @@ -0,0 +1,139 @@ +/* + * 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 from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; + +import { DataStream } from '../../../../../../common/types'; +import { reactRouterNavigate } from '../../../../../shared_imports'; +import { encodePathForReactRouter } from '../../../../services/routing'; + +interface Props { + dataStreams?: DataStream[]; + reload: () => {}; + history: ScopedHistory; + filters?: string; +} + +export const DataStreamTable: React.FunctionComponent = ({ + dataStreams, + reload, + history, + filters, +}) => { + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.nameColumnTitle', { + defaultMessage: 'Name', + }), + truncateText: true, + sortable: true, + // TODO: Render as a link to open the detail panel + }, + { + field: 'indices', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.indicesColumnTitle', { + defaultMessage: 'Indices', + }), + truncateText: true, + sortable: true, + render: (indices: DataStream['indices'], dataStream) => ( + + {indices.length} + + ), + }, + { + field: 'timeStampField', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.timeStampFieldColumnTitle', { + defaultMessage: 'Timestamp field', + }), + truncateText: true, + sortable: true, + }, + { + field: 'generation', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.generationFieldColumnTitle', { + defaultMessage: 'Generation', + }), + truncateText: true, + sortable: true, + }, + ]; + + const pagination = { + initialPageSize: 20, + pageSizeOptions: [10, 20, 50], + }; + + const sorting = { + sort: { + field: 'name', + direction: 'asc', + }, + } as const; + + const searchConfig = { + query: filters, + box: { + incremental: true, + }, + toolsLeft: undefined /* TODO: Actions menu */, + toolsRight: [ + + + , + ], + }; + + return ( + <> + ({ + 'data-test-subj': 'row', + })} + cellProps={() => ({ + 'data-test-subj': 'cell', + })} + data-test-subj="dataStreamTable" + message={ + + } + /> + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/index.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/index.ts new file mode 100644 index 0000000000000..3922ca5c1d50c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/index.ts @@ -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 { DataStreamTable } from './data_stream_table'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/index.ts similarity index 82% rename from x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts rename to x-pack/plugins/index_management/public/application/sections/home/data_stream_list/index.ts index f03f483c0e821..e2f588cc2a0fb 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export declare function IndexList(match: any): any; +export { DataStreamList } from './data_stream_list'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/home.tsx b/x-pack/plugins/index_management/public/application/sections/home/home.tsx index 0ddf4fefce466..51deaf42cc72c 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/home.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/home.tsx @@ -19,6 +19,7 @@ import { EuiTitle, } from '@elastic/eui'; import { documentationService } from '../../services/documentation'; +import { DataStreamList } from './data_stream_list'; import { IndexList } from './index_list'; import { TemplateList } from './template_list'; import { ComponentTemplateList } from '../../components/component_templates'; @@ -26,11 +27,17 @@ import { breadcrumbService } from '../../services/breadcrumbs'; export enum Section { Indices = 'indices', + DataStreams = 'data_streams', IndexTemplates = 'templates', ComponentTemplates = 'component_templates', } -export const homeSections = [Section.Indices, Section.IndexTemplates, Section.ComponentTemplates]; +export const homeSections = [ + Section.Indices, + Section.DataStreams, + Section.IndexTemplates, + Section.ComponentTemplates, +]; interface MatchParams { section: Section; @@ -47,6 +54,15 @@ export const IndexManagementHome: React.FunctionComponent, }, + { + id: Section.DataStreams, + name: ( + + ), + }, { id: Section.IndexTemplates, name: ( @@ -122,6 +138,11 @@ export const IndexManagementHome: React.FunctionComponent + = ({ history }) => { return (
- +
); -} +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.ts similarity index 100% rename from x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.ts diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.d.ts b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.d.ts new file mode 100644 index 0000000000000..35ddfc4813617 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.d.ts @@ -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 declare function IndexTable(props: any): any; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index f33d486520a29..c3acff087146a 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -37,8 +37,10 @@ import { } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; +import { reactRouterNavigate } from '../../../../../shared_imports'; import { REFRESH_RATE_INDEX_LIST } from '../../../../constants'; import { healthToColor } from '../../../../services'; +import { encodePathForReactRouter } from '../../../../services/routing'; import { AppContextConsumer } from '../../../../app_context'; import { renderBadges } from '../../../../lib/render_badges'; import { NoMatch, PageErrorForbidden } from '../../../../components'; @@ -117,6 +119,7 @@ export class IndexTable extends Component { } } } + componentWillUnmount() { clearInterval(this.interval); } @@ -146,11 +149,14 @@ export class IndexTable extends Component { const newIsSortAscending = sortField === column ? !isSortAscending : true; sortChanged(column, newIsSortAscending); }; + renderFilterError() { const { filterError } = this.state; + if (!filterError) { return; } + return ( <> @@ -169,6 +175,7 @@ export class IndexTable extends Component { ); } + onFilterChanged = ({ query, error }) => { if (error) { this.setState({ filterError: error }); @@ -177,6 +184,7 @@ export class IndexTable extends Component { this.setState({ filterError: null }); } }; + getFilters = (extensionsService) => { const { allIndices } = this.props; return extensionsService.filters.reduce((accum, filterExtension) => { @@ -184,6 +192,7 @@ export class IndexTable extends Component { return [...accum, ...filtersToAdd]; }, []); }; + toggleAll = () => { const allSelected = this.areAllItemsSelected(); if (allSelected) { @@ -243,7 +252,8 @@ export class IndexTable extends Component { } buildRowCell(fieldName, value, index, appServices) { - const { openDetailPanel, filterChanged } = this.props; + const { openDetailPanel, filterChanged, history } = this.props; + if (fieldName === 'health') { return {value}; } else if (fieldName === 'name') { @@ -261,7 +271,19 @@ export class IndexTable extends Component { {renderBadges(index, filterChanged, appServices.extensionsService)} ); + } else if (fieldName === 'data_stream') { + return ( + + {value} + + ); } + return value; } @@ -480,12 +502,14 @@ export class IndexTable extends Component { + {(indicesLoading && allIndices.length === 0) || indicesError ? null : ( {extensionsService.toggles.map((toggle) => { return this.renderToggleControl(toggle); })} + + + {this.renderBanners(extensionsService)} + {indicesError && this.renderError()} + {atLeastOneItemSelected ? ( @@ -523,6 +551,7 @@ export class IndexTable extends Component { /> ) : null} + {(indicesLoading && allIndices.length === 0) || indicesError ? null : ( @@ -572,11 +601,14 @@ export class IndexTable extends Component { )} + {this.renderFilterError()} + + {indices.length > 0 ? (
- + + {this.buildHeader()} + {this.buildRows(services)}
) : ( emptyState )} + + {indices.length > 0 ? this.renderPager() : null} ); diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx index ec2956973d4f6..807229fb36267 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx @@ -38,7 +38,7 @@ import { Error, } from '../../../../../components'; import { useLoadIndexTemplate } from '../../../../../services/api'; -import { decodePath } from '../../../../../services/routing'; +import { decodePathFromReactRouter } from '../../../../../services/routing'; import { SendRequestResponse } from '../../../../../../shared_imports'; import { useServices } from '../../../../../app_context'; import { TabSummary, TabMappings, TabSettings, TabAliases } from '../../template_details/tabs'; @@ -107,7 +107,7 @@ export const LegacyTemplateDetails: React.FunctionComponent = ({ reload, }) => { const { uiMetricService } = useServices(); - const decodedTemplateName = decodePath(templateName); + const decodedTemplateName = decodePathFromReactRouter(templateName); const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( decodedTemplateName, isLegacy diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx index 92fedd5d68f00..edce05018ce39 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -9,12 +9,12 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; -import { reactRouterNavigate } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { SendRequestResponse, reactRouterNavigate } from '../../../../../../shared_imports'; import { TemplateListItem } from '../../../../../../../common'; import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/constants'; import { TemplateDeleteModal } from '../../../../../components'; +import { encodePathForReactRouter } from '../../../../../services/routing'; import { useServices } from '../../../../../app_context'; -import { SendRequestResponse } from '../../../../../../shared_imports'; interface Props { templates: TemplateListItem[]; @@ -52,7 +52,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ {...reactRouterNavigate( history, { - pathname: `/templates/${encodeURIComponent(encodeURIComponent(name))}`, + pathname: `/templates/${encodePathForReactRouter(name)}`, search: `legacy=${Boolean(item._kbnMeta.isLegacy)}`, }, () => uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index 8bdd230f89952..82835c56a3877 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -11,7 +11,7 @@ import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../common'; import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; -import { decodePath, getTemplateDetailsLink } from '../../services/routing'; +import { decodePathFromReactRouter, getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; @@ -26,7 +26,7 @@ export const TemplateClone: React.FunctionComponent { - const decodedTemplateName = decodePath(name); + const decodedTemplateName = decodePathFromReactRouter(name); const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index d3e539989bc96..7cacb5ee97a60 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -11,7 +11,7 @@ import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@e import { TemplateDeserialized } from '../../../../common'; import { breadcrumbService } from '../../services/breadcrumbs'; import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; -import { decodePath, getTemplateDetailsLink } from '../../services/routing'; +import { decodePathFromReactRouter, getTemplateDetailsLink } from '../../services/routing'; import { SectionLoading, SectionError, TemplateForm, Error } from '../../components'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; @@ -26,7 +26,7 @@ export const TemplateEdit: React.FunctionComponent { - const decodedTemplateName = decodePath(name); + const decodedTemplateName = decodePathFromReactRouter(name); const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index 8ba53788a92a0..6743e7a7c4403 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -31,15 +31,13 @@ import { UIM_TEMPLATE_UPDATE, UIM_TEMPLATE_CLONE, } from '../../../common/constants'; - +import { TemplateDeserialized, TemplateListItem, DataStream } from '../../../common'; +import { IndexMgmtMetricsType } from '../../types'; +import { doMappingsHaveType } from '../components/mappings_editor'; import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants'; - import { useRequest, sendRequest } from './use_request'; import { httpService } from './http'; import { UiMetricService } from './ui_metric'; -import { TemplateDeserialized, TemplateListItem } from '../../../common'; -import { IndexMgmtMetricsType } from '../../types'; -import { doMappingsHaveType } from '../components/mappings_editor'; // Temporary hack to provide the uiMetricService instance to this file. // TODO: Refactor and export an ApiService instance through the app dependencies context @@ -49,6 +47,21 @@ export const setUiMetricService = (_uiMetricService: UiMetricService({ + path: `${API_BASE_PATH}/data_streams`, + method: 'get', + }); +} + +// TODO: Implement this API endpoint once we have content to surface in the detail panel. +export function useLoadDataStream(name: string) { + return useRequest({ + path: `${API_BASE_PATH}/data_stream/${encodeURIComponent(name)}`, + method: 'get', + }); +} + export async function loadIndices() { const response = await httpService.httpClient.get(`${API_BASE_PATH}/indices`); return response.data ? response.data : response; diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts index a999c58f5bb42..2a895196189d0 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -6,10 +6,8 @@ export const getTemplateListLink = () => `/templates`; -// Need to add some additonal encoding/decoding logic to work with React Router -// For background, see: https://github.com/ReactTraining/history/issues/505 export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHash = false) => { - const baseUrl = `/templates/${encodeURIComponent(encodeURIComponent(name))}`; + const baseUrl = `/templates/${encodePathForReactRouter(name)}`; let url = withHash ? `#${baseUrl}` : baseUrl; if (isLegacy) { url = `${url}?legacy=${isLegacy}`; @@ -18,18 +16,14 @@ export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHas }; export const getTemplateEditLink = (name: string, isLegacy?: boolean) => { - return encodeURI( - `/edit_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` - ); + return encodeURI(`/edit_template/${encodePathForReactRouter(name)}?legacy=${isLegacy === true}`); }; export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => { - return encodeURI( - `/clone_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` - ); + return encodeURI(`/clone_template/${encodePathForReactRouter(name)}?legacy=${isLegacy === true}`); }; -export const decodePath = (pathname: string): string => { +export const decodePathFromReactRouter = (pathname: string): string => { let decodedPath; try { decodedPath = decodeURI(pathname); @@ -39,3 +33,8 @@ export const decodePath = (pathname: string): string => { } return decodeURIComponent(decodedPath); }; + +// Need to add some additonal encoding/decoding logic to work with React Router +// For background, see: https://github.com/ReactTraining/history/issues/505 +export const encodePathForReactRouter = (pathname: string): string => + encodeURIComponent(encodeURIComponent(pathname)); diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index 8942367261511..afd5a5cf650e1 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -31,3 +31,5 @@ export { export { getFormRow, Field } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { isJSON } from '../../../../src/plugins/es_ui_shared/static/validators/string'; + +export { reactRouterNavigate } from '../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts index b51f7d924dba7..6b1bf47512b21 100644 --- a/x-pack/plugins/index_management/server/client/elasticsearch.ts +++ b/x-pack/plugins/index_management/server/client/elasticsearch.ts @@ -10,6 +10,46 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) Client.prototype.dataManagement = components.clientAction.namespaceFactory(); const dataManagement = Client.prototype.dataManagement.prototype; + // Data streams + dataManagement.getDataStreams = ca({ + urls: [ + { + fmt: '/_data_stream', + }, + ], + 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({ + urls: [ + { + fmt: '/_data_stream/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'PUT', + }); + + dataManagement.deleteDataStream = ca({ + urls: [ + { + fmt: '/_data_stream/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); + // Component templates dataManagement.getComponentTemplates = ca({ urls: [ @@ -71,4 +111,33 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ], method: 'GET', }); + + dataManagement.saveComposableIndexTemplate = ca({ + urls: [ + { + fmt: '/_index_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'PUT', + }); + + dataManagement.deleteComposableIndexTemplate = ca({ + urls: [ + { + fmt: '/_index_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); }; diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts new file mode 100644 index 0000000000000..56c514e30f242 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { RouteDependencies } from '../../../types'; + +import { registerGetAllRoute } from './register_get_route'; + +export function registerDataStreamRoutes(dependencies: RouteDependencies) { + registerGetAllRoute(dependencies); +} diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts new file mode 100644 index 0000000000000..9128556130bf4 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts @@ -0,0 +1,34 @@ +/* + * 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 { deserializeDataStreamList } from '../../../../common/lib'; +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +export function registerGetAllRoute({ router, license, lib: { isEsError } }: RouteDependencies) { + router.get( + { path: addBasePath('/data_streams'), validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + + try { + const dataStreams = await callAsCurrentUser('dataManagement.getDataStreams'); + const body = deserializeDataStreamList(dataStreams); + + return res.ok({ body }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +} diff --git a/x-pack/plugins/index_management/server/routes/index.ts b/x-pack/plugins/index_management/server/routes/index.ts index 1e5aaf8087624..202e6919f7b13 100644 --- a/x-pack/plugins/index_management/server/routes/index.ts +++ b/x-pack/plugins/index_management/server/routes/index.ts @@ -6,6 +6,7 @@ import { RouteDependencies } from '../types'; +import { registerDataStreamRoutes } from './api/data_streams'; import { registerIndicesRoutes } from './api/indices'; import { registerTemplateRoutes } from './api/templates'; import { registerMappingRoute } from './api/mapping'; @@ -15,6 +16,7 @@ import { registerComponentTemplateRoutes } from './api/component_templates'; export class ApiRoutes { setup(dependencies: RouteDependencies) { + registerDataStreamRoutes(dependencies); registerIndicesRoutes(dependencies); registerTemplateRoutes(dependencies); registerSettingsRoutes(dependencies); diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts new file mode 100644 index 0000000000000..9a8511b4331ea --- /dev/null +++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts @@ -0,0 +1,89 @@ +/* + * 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. + */ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +// @ts-ignore +import { API_BASE_PATH } from './constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + const createDataStream = (name: string) => { + // A data stream requires an index template before it can be created. + return es.dataManagement + .saveComposableIndexTemplate({ + name, + body: { + index_patterns: ['*'], + template: { + settings: {}, + }, + data_stream: { + timestamp_field: '@timestamp', + }, + }, + }) + .then(() => + es.dataManagement.createDataStream({ + name, + }) + ); + }; + + const deleteDataStream = (name: string) => { + return es.dataManagement + .deleteComposableIndexTemplate({ + name, + }) + .then(() => + es.dataManagement.deleteDataStream({ + name, + }) + ); + }; + + describe('Data streams', function () { + const testDataStreamName = 'test-data-stream'; + + describe('Get', () => { + before(async () => await createDataStream(testDataStreamName)); + after(async () => await deleteDataStream(testDataStreamName)); + + describe('all data streams', () => { + it('returns an array of data streams', async () => { + const { body: dataStreams } = await supertest + .get(`${API_BASE_PATH}/data_streams`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + // ES determines these values so we'll just echo them back. + const { name: indexName, uuid } = dataStreams[0].indices[0]; + expect(dataStreams).to.eql([ + { + name: testDataStreamName, + timeStampField: '@timestamp', + indices: [ + { + name: indexName, + uuid, + }, + ], + generation: 1, + }, + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/management/index_management/index.js b/x-pack/test/api_integration/apis/management/index_management/index.js index fdee325938ff4..93e8a4a8d6130 100644 --- a/x-pack/test/api_integration/apis/management/index_management/index.js +++ b/x-pack/test/api_integration/apis/management/index_management/index.js @@ -10,6 +10,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./mapping')); loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./stats')); + loadTestFile(require.resolve('./data_streams')); loadTestFile(require.resolve('./templates')); loadTestFile(require.resolve('./component_templates')); }); diff --git a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js index b950a56a913db..1a1517567eaed 100644 --- a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js +++ b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { getRandomString } from './random'; /** diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts index cca0a5d1ad1bd..90bc3603c1613 100644 --- a/x-pack/test/functional/apps/index_management/home_page.ts +++ b/x-pack/test/functional/apps/index_management/home_page.ts @@ -18,12 +18,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.common.navigateToApp('indexManagement'); }); - it('Loads the app', async () => { + it('Loads the app and renders the indices tab by default', async () => { await log.debug('Checking for section heading to say Index Management.'); const headingText = await pageObjects.indexManagement.sectionHeadingText(); expect(headingText).to.be('Index Management'); + // Verify url + const url = await browser.getCurrentUrl(); + expect(url).to.contain(`/indices`); + + // Verify content const indicesList = await testSubjects.exists('indicesList'); expect(indicesList).to.be(true); @@ -31,6 +36,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await reloadIndicesButton.isDisplayed()).to.be(true); }); + describe('Data streams', () => { + it('renders the data streams tab', async () => { + // Navigate to the data streams tab + await pageObjects.indexManagement.changeTabs('data_streamsTab'); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify url + const url = await browser.getCurrentUrl(); + expect(url).to.contain(`/data_streams`); + + // Verify content + const dataStreamList = await testSubjects.exists('dataStreamList'); + expect(dataStreamList).to.be(true); + }); + }); + describe('Index templates', () => { it('renders the index templates tab', async () => { // Navigate to the index templates tab diff --git a/x-pack/test/functional/page_objects/index_management_page.ts b/x-pack/test/functional/page_objects/index_management_page.ts index 9bfcd79671b4d..5e5d0e7583450 100644 --- a/x-pack/test/functional/page_objects/index_management_page.ts +++ b/x-pack/test/functional/page_objects/index_management_page.ts @@ -44,7 +44,10 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext) }; }); }, - async changeTabs(tab: 'indicesTab' | 'templatesTab' | 'component_templatesTab') { + + async changeTabs( + tab: 'indicesTab' | 'data_streamsTab' | 'templatesTab' | 'component_templatesTab' + ) { await testSubjects.click(tab); }, };