diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx index 0a27b4098681..732aa35b0523 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx @@ -13,7 +13,7 @@ import { Error } from '../types'; interface Props { title: React.ReactNode; - error: Error; + error?: Error; actions?: JSX.Element; isCentered?: boolean; } @@ -32,30 +32,30 @@ export const PageError: React.FunctionComponent = ({ isCentered, ...rest }) => { - const { - error: errorString, - cause, // wrapEsError() on the server adds a "cause" array - message, - } = error; + const errorString = error?.error; + const cause = error?.cause; // wrapEsError() on the server adds a "cause" array + const message = error?.message; const errorContent = ( {title}} body={ - <> - {cause ? message || errorString :

{message || errorString}

} - {cause && ( - <> - -
    - {cause.map((causeMsg, i) => ( -
  • {causeMsg}
  • - ))} -
- - )} - + error && ( + <> + {cause ? message || errorString :

{message || errorString}

} + {cause && ( + <> + +
    + {cause.map((causeMsg, i) => ( +
  • {causeMsg}
  • + ))} +
+ + )} + + ) } iconType="alert" actions={actions} diff --git a/src/plugins/es_ui_shared/public/components/page_loading/index.ts b/src/plugins/es_ui_shared/public/components/page_loading/index.ts new file mode 100644 index 000000000000..3e7b93bb4e7c --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/page_loading/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { PageLoading } from './page_loading'; diff --git a/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx new file mode 100644 index 000000000000..2fb99208e58a --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText, EuiPageContent } from '@elastic/eui'; + +export const PageLoading: React.FunctionComponent = ({ children }) => { + return ( + + } + body={{children}} + data-test-subj="sectionLoading" + /> + + ); +}; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 7b9013c043a0..ef2e2daa2546 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -17,6 +17,7 @@ import * as XJson from './xjson'; export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './components/json_editor'; +export { PageLoading } from './components/page_loading'; export { SectionLoading } from './components/section_loading'; export { Frequency, CronEditor } from './components/cron_editor'; diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index 4ac94319d471..463d0b30cad0 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -6,9 +6,12 @@ */ import React from 'react'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; +import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; +import axios from 'axios'; +import sinon from 'sinon'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import axiosXhrAdapter from 'axios/lib/adapters/xhr'; /** * The below import is required to avoid a console error warn from brace package @@ -18,9 +21,9 @@ import { MemoryRouter } from 'react-router-dom'; */ import { mountWithIntl, stubWebWorker } from '@kbn/test/jest'; // eslint-disable-line no-unused-vars +import { BASE_PATH, API_BASE_PATH } from '../../common/constants'; import { AppWithoutRouter } from '../../public/application/app'; import { AppContextProvider } from '../../public/application/app_context'; -import { Provider } from 'react-redux'; import { loadIndicesSuccess } from '../../public/application/store/actions'; import { breadcrumbService } from '../../public/application/services/breadcrumbs'; import { UiMetricService } from '../../public/application/services/ui_metric'; @@ -29,10 +32,7 @@ import { httpService } from '../../public/application/services/http'; import { setUiMetricService } from '../../public/application/services/api'; import { indexManagementStore } from '../../public/application/store'; import { setExtensionsService } from '../../public/application/store/selectors/extension_service'; -import { BASE_PATH, API_BASE_PATH } from '../../common/constants'; import { ExtensionsService } from '../../public/services'; -import sinon from 'sinon'; -import { findTestSubject } from '@elastic/eui/lib/test'; /* eslint-disable @kbn/eslint/no-restricted-paths */ import { notificationServiceMock } from '../../../../../src/core/public/notifications/notifications_service.mock'; @@ -40,9 +40,9 @@ import { notificationServiceMock } from '../../../../../src/core/public/notifica const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); let server = null; - let store = null; const indices = []; + for (let i = 0; i < 105; i++) { const baseFake = { health: i % 2 === 0 ? 'green' : 'yellow', @@ -63,8 +63,12 @@ for (let i = 0; i < 105; i++) { name: `.admin${i}`, }); } + let component = null; +// Resolve outstanding API requests. See https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/ +const runAllPromises = () => new Promise(setImmediate); + const status = (rendered, row = 0) => { rendered.update(); return findTestSubject(rendered, 'indexTableCell-status') @@ -76,39 +80,54 @@ const status = (rendered, row = 0) => { const snapshot = (rendered) => { expect(rendered).toMatchSnapshot(); }; + const openMenuAndClickButton = (rendered, rowIndex, buttonIndex) => { + // Select a row. const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(rowIndex).simulate('change', { target: { checked: true } }); rendered.update(); + + // Click the bulk actions button to open the context menu. const actionButton = findTestSubject(rendered, 'indexActionsContextMenuButton'); actionButton.simulate('click'); rendered.update(); + + // Click an action in the context menu. const contextMenuButtons = findTestSubject(rendered, 'indexTableContextMenuButton'); contextMenuButtons.at(buttonIndex).simulate('click'); + rendered.update(); }; -const testEditor = (buttonIndex, rowIndex = 0) => { - const rendered = mountWithIntl(component); + +const testEditor = (rendered, buttonIndex, rowIndex = 0) => { openMenuAndClickButton(rendered, rowIndex, buttonIndex); rendered.update(); snapshot(findTestSubject(rendered, 'detailPanelTabSelected').text()); }; -const testAction = (buttonIndex, done, rowIndex = 0) => { - const rendered = mountWithIntl(component); - let count = 0; + +const testAction = (rendered, buttonIndex, rowIndex = 0) => { + // This is leaking some implementation details about how Redux works. Not sure exactly what's going on + // but it looks like we're aware of how many Redux actions are dispatched in response to user interaction, + // so we "time" our assertion based on how many Redux actions we observe. This is brittle because it + // depends upon how our UI is architected, which will affect how many actions are dispatched. + // Expect this to break when we rearchitect the UI. + let dispatchedActionsCount = 0; store.subscribe(() => { - if (count > 1) { + if (dispatchedActionsCount === 1) { + // Take snapshot of final state. snapshot(status(rendered, rowIndex)); - done(); } - count++; + dispatchedActionsCount++; }); - expect.assertions(2); + openMenuAndClickButton(rendered, rowIndex, buttonIndex); + // take snapshot of initial state. snapshot(status(rendered, rowIndex)); }; + const names = (rendered) => { return findTestSubject(rendered, 'indexTableIndexNameLink'); }; + const namesText = (rendered) => { return names(rendered).map((button) => button.text()); }; @@ -142,23 +161,28 @@ describe('index table', () => { ); + store.dispatch(loadIndicesSuccess({ indices })); server = sinon.fakeServer.create(); + server.respondWith(`${API_BASE_PATH}/indices`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(indices), ]); + server.respondWith([ 200, { 'Content-Type': 'application/json' }, JSON.stringify({ acknowledged: true }), ]); + server.respondWith(`${API_BASE_PATH}/indices/reload`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(indices), ]); + server.respondImmediately = true; }); afterEach(() => { @@ -168,83 +192,124 @@ describe('index table', () => { server.restore(); }); - test('should change pages when a pagination link is clicked on', () => { + test('should change pages when a pagination link is clicked on', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + snapshot(namesText(rendered)); + const pagingButtons = rendered.find('.euiPaginationButton'); pagingButtons.at(2).simulate('click'); - rendered.update(); snapshot(namesText(rendered)); }); - test('should show more when per page value is increased', () => { + + test('should show more when per page value is increased', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button'); perPageButton.simulate('click'); rendered.update(); + const fiftyButton = rendered.find('.euiContextMenuItem').at(1); fiftyButton.simulate('click'); rendered.update(); expect(namesText(rendered).length).toBe(50); }); - test('should show the Actions menu button only when at least one row is selected', () => { + + test('should show the Actions menu button only when at least one row is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + let button = findTestSubject(rendered, 'indexTableContextMenuButton'); expect(button.length).toEqual(0); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); rendered.update(); button = findTestSubject(rendered, 'indexActionsContextMenuButton'); expect(button.length).toEqual(1); }); - test('should update the Actions menu button text when more than one row is selected', () => { + + test('should update the Actions menu button text when more than one row is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + let button = findTestSubject(rendered, 'indexTableContextMenuButton'); expect(button.length).toEqual(0); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); rendered.update(); button = findTestSubject(rendered, 'indexActionsContextMenuButton'); expect(button.text()).toEqual('Manage index'); + checkboxes.at(1).simulate('change', { target: { checked: true } }); rendered.update(); button = findTestSubject(rendered, 'indexActionsContextMenuButton'); expect(button.text()).toEqual('Manage 2 indices'); }); - test('should show system indices only when the switch is turned on', () => { + + test('should show system indices only when the switch is turned on', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + snapshot(rendered.find('.euiPagination li').map((item) => item.text())); const switchControl = rendered.find('.euiSwitch__button'); switchControl.simulate('click'); snapshot(rendered.find('.euiPagination li').map((item) => item.text())); }); - test('should filter based on content of search input', () => { + + test('should filter based on content of search input', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const searchInput = rendered.find('.euiFieldSearch').first(); searchInput.instance().value = 'testy0'; searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); rendered.update(); snapshot(namesText(rendered)); }); - test('should sort when header is clicked', () => { + + test('should sort when header is clicked', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const nameHeader = findTestSubject(rendered, 'indexTableHeaderCell-name').find('button'); nameHeader.simulate('click'); rendered.update(); snapshot(namesText(rendered)); + nameHeader.simulate('click'); rendered.update(); snapshot(namesText(rendered)); }); - test('should open the index detail slideout when the index name is clicked', () => { + + test('should open the index detail slideout when the index name is clicked', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(0); + const indexNameLink = names(rendered).at(0); indexNameLink.simulate('click'); rendered.update(); expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(1); }); - test('should show the right context menu options when one index is selected and open', () => { + + test('should show the right context menu options when one index is selected and open', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); rendered.update(); @@ -253,8 +318,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when one index is selected and closed', () => { + + test('should show the right context menu options when one index is selected and closed', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(1).simulate('change', { target: { checked: true } }); rendered.update(); @@ -263,8 +332,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when one open and one closed index is selected', () => { + + test('should show the right context menu options when one open and one closed index is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); checkboxes.at(1).simulate('change', { target: { checked: true } }); @@ -274,8 +347,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when more than one open index is selected', () => { + + test('should show the right context menu options when more than one open index is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); checkboxes.at(2).simulate('change', { target: { checked: true } }); @@ -285,8 +362,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when more than one closed index is selected', () => { + + test('should show the right context menu options when more than one closed index is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(1).simulate('change', { target: { checked: true } }); checkboxes.at(3).simulate('change', { target: { checked: true } }); @@ -296,37 +377,57 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('flush button works from context menu', (done) => { - testAction(8, done); - }); - test('clear cache button works from context menu', (done) => { - testAction(7, done); - }); - test('refresh button works from context menu', (done) => { - testAction(6, done); - }); - test('force merge button works from context menu', (done) => { + + test('flush button works from context menu', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 8); + }); + + test('clear cache button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 7); + }); + + test('refresh button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 6); + }); + + test('force merge button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const rowIndex = 0; openMenuAndClickButton(rendered, rowIndex, 5); snapshot(status(rendered, rowIndex)); expect(rendered.find('.euiModal').length).toBe(1); + let count = 0; store.subscribe(() => { - if (count > 1) { + if (count === 1) { snapshot(status(rendered, rowIndex)); expect(rendered.find('.euiModal').length).toBe(0); - done(); } count++; }); + const confirmButton = findTestSubject(rendered, 'confirmModalConfirmButton'); confirmButton.simulate('click'); snapshot(status(rendered, rowIndex)); }); - // Commenting the following 2 tests as it works in the browser (status changes to "closed" or "open") but the - // snapshot say the contrary. Need to be investigated. - test('close index button works from context menu', (done) => { + + test('close index button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const modifiedIndices = indices.map((index) => { return { ...index, @@ -339,32 +440,56 @@ describe('index table', () => { { 'Content-Type': 'application/json' }, JSON.stringify(modifiedIndices), ]); - testAction(4, done); + + testAction(rendered, 4); }); - test('open index button works from context menu', (done) => { + + test('open index button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const modifiedIndices = indices.map((index) => { return { ...index, status: index.name === 'testy1' ? 'open' : index.status, }; }); + server.respondWith(`${API_BASE_PATH}/indices/reload`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(modifiedIndices), ]); - testAction(3, done, 1); + + testAction(rendered, 3, 1); }); - test('show settings button works from context menu', () => { - testEditor(0); + + test('show settings button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 0); }); - test('show mappings button works from context menu', () => { - testEditor(1); + + test('show mappings button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 1); }); - test('show stats button works from context menu', () => { - testEditor(2); + + test('show stats button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 2); }); - test('edit index button works from context menu', () => { - testEditor(3); + + test('edit index button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 3); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts index 8c8f7e578992..dee15f2ae3a4 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts @@ -165,8 +165,10 @@ describe('', () => { const { exists, find } = testBed; expect(exists('componentTemplatesLoadError')).toBe(true); + // The text here looks weird because the child elements' text values (title and description) + // are concatenated when we retrive the error element's text value. expect(find('componentTemplatesLoadError').text()).toContain( - 'Unable to load component templates. Try again.' + 'Error loading component templatesInternal server error' ); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 2bb240e6b6ae..77668f7d5507 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -13,8 +13,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ScopedHistory } from 'kibana/public'; import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; -import { attemptToURIDecode } from '../../../../shared_imports'; -import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports'; +import { + APP_WRAPPER_CLASS, + PageLoading, + PageError, + attemptToURIDecode, +} from '../../../../shared_imports'; +import { ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants'; import { useComponentTemplatesContext } from '../component_templates_context'; import { @@ -24,7 +29,6 @@ import { } from '../component_template_details'; import { EmptyPrompt } from './empty_prompt'; import { ComponentTable } from './table'; -import { LoadError } from './error'; import { ComponentTemplatesDeleteModal } from './delete_modal'; interface Props { @@ -138,18 +142,20 @@ export const ComponentTemplateList: React.FunctionComponent = ({ } }, [componentTemplateName, removeContentFromGlobalFlyout]); - let content: React.ReactNode; - if (isLoading) { - content = ( - + return ( + - + ); - } else if (data?.length) { + } + + let content: React.ReactNode; + + if (data?.length) { content = ( <> @@ -183,11 +189,22 @@ export const ComponentTemplateList: React.FunctionComponent = ({ } else if (data && data.length === 0) { content = ; } else if (error) { - content = ; + content = ( + + } + error={error} + data-test-subj="componentTemplatesLoadError" + /> + ); } return ( -
+
{content} {/* delete modal */} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx deleted file mode 100644 index 9fd0031fe877..000000000000 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink, EuiCallOut } from '@elastic/eui'; - -export interface Props { - onReloadClick: () => void; -} - -export const LoadError: FunctionComponent = ({ onReloadClick }) => { - return ( - - - - ), - }} - /> - } - /> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx index a0f6dc4b59fe..eecb56768df9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx @@ -9,10 +9,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; import { - SectionError, + PageLoading, + PageError, useAuthorizationContext, WithPrivileges, - SectionLoading, NotAuthorizedSection, } from '../shared_imports'; import { APP_CLUSTER_REQUIRED_PRIVILEGES } from '../constants'; @@ -26,7 +26,7 @@ export const ComponentTemplatesWithPrivileges: FunctionComponent = ({ if (apiError) { return ( - { if (isLoading) { return ( - + - + ); } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx index b87b043c924a..d19c500c3622 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx @@ -10,7 +10,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SectionLoading, attemptToURIDecode } from '../../shared_imports'; +import { PageLoading, attemptToURIDecode } from '../../shared_imports'; import { useComponentTemplatesContext } from '../../component_templates_context'; import { ComponentTemplateCreate } from '../component_template_create'; @@ -30,7 +30,8 @@ export const ComponentTemplateClone: FunctionComponent { if (error && !isLoading) { - toasts.addError(error, { + // Toasts expects a generic Error object, which is typed as having a required name property. + toasts.addError({ ...error, name: '' } as Error, { title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', { defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`, values: { sourceComponentTemplateName }, @@ -42,12 +43,12 @@ export const ComponentTemplateClone: FunctionComponent + - + ); } else { // We still show the create form (unpopulated) even if we were not able to load the diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx index 5163c75bdbad..8fe2c193daa0 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx @@ -8,7 +8,7 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiPageContentBody, EuiSpacer, EuiPageHeader } from '@elastic/eui'; import { ComponentTemplateDeserialized } from '../../shared_imports'; import { useComponentTemplatesContext } from '../../component_templates_context'; @@ -59,27 +59,28 @@ export const ComponentTemplateCreate: React.FunctionComponent - - -

+ + -

-
+ + } + bottomBorder + /> - + - -
- + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx index 809fac980069..6ac831b5dacc 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -8,13 +8,15 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { EuiPageContentBody, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { useComponentTemplatesContext } from '../../component_templates_context'; import { ComponentTemplateDeserialized, - SectionLoading, + PageLoading, + PageError, attemptToURIDecode, + Error, } from '../../shared_imports'; import { ComponentTemplateForm } from '../component_template_form'; @@ -65,64 +67,57 @@ export const ComponentTemplateEdit: React.FunctionComponent + return ( + - + ); - } else if (error) { - content = ( - <> - + } + error={error as Error} + data-test-subj="loadComponentTemplateError" + /> + ); + } + + return ( + + - } - color="danger" - iconType="alert" - data-test-subj="loadComponentTemplateError" - > -
{error.message}
-
- - - ); - } else if (componentTemplate) { - content = ( + + } + bottomBorder + /> + + + - ); - } - - return ( - - - -

- -

-
- - {content} -
-
+ ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 75c68e71996b..6bf6d204fd9a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -10,7 +10,6 @@ import { ComponentTemplateListItem, ComponentTemplateDeserialized, ComponentTemplateSerialized, - Error, } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, @@ -26,7 +25,7 @@ export const getApi = ( trackMetric: (type: UiCounterMetricType, eventName: string) => void ) => { function useLoadComponentTemplates() { - return useRequest({ + return useRequest({ path: `${apiBasePath}/component_templates`, method: 'get', }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts index 64b2e6b47e5d..a7056e27b5ca 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts @@ -14,6 +14,7 @@ import { SendRequestResponse, sendRequest as _sendRequest, useRequest as _useRequest, + Error, } from '../shared_imports'; export type UseRequestHook = ( diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index afc7aed87438..15528f5b4e8e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -12,10 +12,12 @@ export { SendRequestResponse, sendRequest, useRequest, - SectionLoading, WithPrivileges, AuthorizationProvider, SectionError, + SectionLoading, + PageLoading, + PageError, Error, useAuthorizationContext, NotAuthorizedSection, diff --git a/x-pack/plugins/index_management/public/application/components/index.ts b/x-pack/plugins/index_management/public/application/components/index.ts index f5c58e5b45eb..eeba6e16b543 100644 --- a/x-pack/plugins/index_management/public/application/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/index.ts @@ -6,9 +6,7 @@ */ export { SectionError, Error } from './section_error'; -export { SectionLoading } from './section_loading'; export { NoMatch } from './no_match'; -export { PageErrorForbidden } from './page_error'; export { TemplateDeleteModal } from './template_delete_modal'; export { TemplateForm } from './template_form'; export { DataHealth } from './data_health'; diff --git a/x-pack/plugins/index_management/public/application/components/page_error/index.ts b/x-pack/plugins/index_management/public/application/components/page_error/index.ts deleted file mode 100644 index 040edfa362c6..000000000000 --- a/x-pack/plugins/index_management/public/application/components/page_error/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { PageErrorForbidden } from './page_error_forbidden'; diff --git a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx b/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx deleted file mode 100644 index e22b180881ed..000000000000 --- a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiEmptyPrompt, EuiPageContent } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export function PageErrorForbidden() { - return ( - - - - - } - /> - - ); -} diff --git a/x-pack/plugins/index_management/public/application/components/section_loading.tsx b/x-pack/plugins/index_management/public/application/components/section_loading.tsx deleted file mode 100644 index 3c31744dee39..000000000000 --- a/x-pack/plugins/index_management/public/application/components/section_loading.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; - -interface Props { - children: React.ReactNode; -} - -export const SectionLoading: React.FunctionComponent = ({ children }) => { - return ( - } - body={{children}} - data-test-subj="sectionLoading" - /> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 54160141827d..4ccd77d275a9 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -8,7 +8,7 @@ import React, { useState, useCallback, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiButton } from '@elastic/eui'; +import { EuiSpacer, EuiButton, EuiPageHeader } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { TemplateDeserialized } from '../../../../common'; @@ -292,7 +292,7 @@ export const TemplateForm = ({ return ( <> {/* Form header */} - {title} + {title}} bottomBorder /> 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 index a9258c6a3b10..3d5f56c08f8e 100644 --- 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 @@ -24,8 +24,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { reactRouterNavigate } from '../../../../../shared_imports'; -import { SectionLoading, SectionError, Error, DataHealth } from '../../../../components'; +import { SectionLoading, reactRouterNavigate } from '../../../../../shared_imports'; +import { SectionError, Error, DataHealth } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; 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 index 131dc2662bc1..7bd7c163837d 100644 --- 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 @@ -16,18 +16,22 @@ import { EuiText, EuiIconTip, EuiSpacer, + EuiPageContent, EuiEmptyPrompt, EuiLink, } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { + PageLoading, + PageError, + Error, reactRouterNavigate, extractQueryParams, attemptToURIDecode, + APP_WRAPPER_CLASS, } from '../../../../shared_imports'; import { useAppContext } from '../../../app_context'; -import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadDataStreams } from '../../../services/api'; import { documentationService } from '../../../services/documentation'; import { Section } from '../home'; @@ -166,16 +170,16 @@ export const DataStreamList: React.FunctionComponent + - + ); } else if (error) { content = ( - ); - } else if (Array.isArray(dataStreams) && dataStreams.length > 0) { - activateHiddenFilter(isSelectedDataStreamHidden(dataStreams, decodedDataStreamName)); + } else { + activateHiddenFilter(isSelectedDataStreamHidden(dataStreams!, decodedDataStreamName)); content = ( - <> + {renderHeader()} @@ -270,12 +274,12 @@ export const DataStreamList: React.FunctionComponent - + ); } return ( -
+
{content} {/* diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx index ac46b5dbd256..fc68ca33e953 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx @@ -8,12 +8,13 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { APP_WRAPPER_CLASS } from '../../../../shared_imports'; import { DetailPanel } from './detail_panel'; import { IndexTable } from './index_table'; export const IndexList: React.FunctionComponent = ({ history }) => { return ( -
+
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 f488290692e7..0a407927e346 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 @@ -19,7 +19,7 @@ import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, - EuiLoadingSpinner, + EuiPageContent, EuiScreenReaderOnly, EuiSpacer, EuiSearchBar, @@ -37,13 +37,18 @@ import { } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; -import { reactRouterNavigate, attemptToURIDecode } from '../../../../../shared_imports'; +import { + PageLoading, + PageError, + reactRouterNavigate, + attemptToURIDecode, +} from '../../../../../shared_imports'; import { REFRESH_RATE_INDEX_LIST } from '../../../../constants'; import { getDataStreamDetailsLink } from '../../../../services/routing'; import { documentationService } from '../../../../services/documentation'; import { AppContextConsumer } from '../../../../app_context'; import { renderBadges } from '../../../../lib/render_badges'; -import { NoMatch, PageErrorForbidden, DataHealth } from '../../../../components'; +import { NoMatch, DataHealth } from '../../../../components'; import { IndexActionsContextMenu } from '../index_actions_context_menu'; const HEADERS = { @@ -332,42 +337,6 @@ export class IndexTable extends Component { }); } - renderError() { - const { indicesError } = this.props; - - const data = indicesError.body ? indicesError.body : indicesError; - - const { error: errorString, cause, message } = data; - - return ( - - - } - color="danger" - iconType="alert" - > -
{message || errorString}
- {cause && ( - - -
    - {cause.map((message, i) => ( -
  • {message}
  • - ))} -
-
- )} -
- -
- ); - } - renderBanners(extensionsService) { const { allIndices = [], filterChanged } = this.props; return extensionsService.banners.map((bannerExtension, i) => { @@ -470,37 +439,71 @@ export class IndexTable extends Component { } = this.props; const { includeHiddenIndices } = this.readURLParams(); + const hasContent = !indicesLoading && !indicesError; - let emptyState; + if (!hasContent) { + const renderNoContent = () => { + if (indicesLoading) { + return ( + + + + ); + } - if (indicesLoading) { - emptyState = ( - - - - - + if (indicesError) { + if (indicesError.status === 403) { + return ( + + } + /> + ); + } + + return ( + + } + error={indicesError.body} + /> + ); + } + }; + + return ( + + {renderNoContent()} + ); } - if (!indicesLoading && !indicesError) { - emptyState = ; - } - const { selectedIndicesMap } = this.state; const atLeastOneItemSelected = Object.keys(selectedIndicesMap).length > 0; - if (indicesError && indicesError.status === 403) { - return ; - } - return ( {({ services }) => { const { extensionsService } = services; return ( - + @@ -557,8 +560,6 @@ export class IndexTable extends Component { {this.renderBanners(extensionsService)} - {indicesError && this.renderError()} - {atLeastOneItemSelected ? ( @@ -665,13 +666,13 @@ export class IndexTable extends Component {
) : ( - emptyState + )} {indices.length > 0 ? this.renderPager() : null} - + ); }} diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx index e61362efb8c9..1a82cb3bfbdd 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -33,8 +33,8 @@ import { UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB, } from '../../../../../../common/constants'; -import { UseRequestResponse } from '../../../../../shared_imports'; -import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; +import { SectionLoading, UseRequestResponse } from '../../../../../shared_imports'; +import { TemplateDeleteModal, SectionError, Error } from '../../../../components'; import { useLoadIndexTemplate } from '../../../../services/api'; import { useServices } from '../../../../app_context'; import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index b8b5a8e3c7d1..57f18134be5d 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -24,13 +24,14 @@ import { import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants'; import { TemplateListItem } from '../../../../../common'; -import { attemptToURIDecode } from '../../../../shared_imports'; import { - SectionError, - SectionLoading, - Error, - LegacyIndexTemplatesDeprecation, -} from '../../../components'; + APP_WRAPPER_CLASS, + PageLoading, + PageError, + attemptToURIDecode, + reactRouterNavigate, +} from '../../../../shared_imports'; +import { LegacyIndexTemplatesDeprecation } from '../../../components'; import { useLoadIndexTemplates } from '../../../services/api'; import { documentationService } from '../../../services/documentation'; import { useServices } from '../../../app_context'; @@ -130,7 +131,8 @@ export const TemplateList: React.FunctionComponent ( - + // flex-grow: 0 is needed here because the parent element is a flex column and the header would otherwise expand. + ); - const renderContent = () => { - if (isLoading) { - return ( - - - - ); - } else if (error) { - return ( - - } - error={error as Error} - /> - ); - } else if (!hasTemplates) { - return ( - - - - } - data-test-subj="emptyPrompt" - /> - ); - } else { - return ( - - {/* Header */} - {renderHeader()} - - {/* Composable index templates table */} - {renderTemplatesTable()} - - {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */} - {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()} - - ); - } - }; - - // Track component loaded + // Track this component mounted. useEffect(() => { uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD); }, [uiMetricService]); - return ( -
- {renderContent()} + let content; - {isTemplateDetailsVisible && ( - + - )} + + ); + } else if (error) { + content = ( + + } + error={error} + /> + ); + } else if (!hasTemplates) { + content = ( + + + + } + body={ + <> +

+ +

+ + } + actions={ + + + + } + data-test-subj="emptyPrompt" + /> + ); + } else { + content = ( + <> + {/* Header */} + {renderHeader()} + + {/* Composable index templates table */} + {renderTemplatesTable()} + + {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */} + {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()} + + {isTemplateDetailsVisible && ( + + )} + + ); + } + + return ( +
+ {content}
); }; 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 36bff298e345..32c84bc3b15f 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 @@ -8,11 +8,12 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; +import { PageLoading, PageError, Error } from '../../../shared_imports'; import { TemplateDeserialized } from '../../../../common'; -import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; +import { TemplateForm } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; import { getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; @@ -62,24 +63,22 @@ export const TemplateClone: React.FunctionComponent { breadcrumbService.setBreadcrumbs('templateClone'); }, []); if (isLoading) { - content = ( - + return ( + - + ); } else if (templateToCloneError) { - content = ( - ); - } else if (templateToClone) { - const templateData = { - ...templateToClone, - name: `${decodedTemplateName}-copy`, - } as TemplateDeserialized; + } - content = ( + const templateData = { + ...templateToClone, + name: `${decodedTemplateName}-copy`, + } as TemplateDeserialized; + + return ( + -

- -

- + } defaultValue={templateData} onSave={onSave} @@ -117,12 +114,6 @@ export const TemplateClone: React.FunctionComponent - ); - } - - return ( - - {content} - +
); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index 310807aeef38..6eba112b1193 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; import { parse } from 'query-string'; import { ScopedHistory } from 'kibana/public'; @@ -52,34 +52,28 @@ export const TemplateCreate: React.FunctionComponent = ({ h }, []); return ( - - - -

- {isLegacy ? ( - - ) : ( - - )} -

- - } - onSave={onSave} - isSaving={isSaving} - saveError={saveError} - clearSaveError={clearSaveError} - isLegacy={isLegacy} - history={history as ScopedHistory} - /> -
-
+ + + ) : ( + + ) + } + onSave={onSave} + isSaving={isSaving} + saveError={saveError} + clearSaveError={clearSaveError} + isLegacy={isLegacy} + history={history as ScopedHistory} + /> + ); }; 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 f4ffe97931a2..ff6909d4666f 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 @@ -7,16 +7,17 @@ import React, { useEffect, useState, Fragment } from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { EuiPageContentBody, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { TemplateDeserialized } from '../../../../common'; -import { attemptToURIDecode } from '../../../shared_imports'; +import { PageError, PageLoading, attemptToURIDecode, Error } from '../../../shared_imports'; import { breadcrumbService } from '../../services/breadcrumbs'; import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; import { getTemplateDetailsLink } from '../../services/routing'; -import { SectionLoading, SectionError, TemplateForm, Error } from '../../components'; +import { TemplateForm } from '../../components'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; interface MatchParams { @@ -62,27 +63,27 @@ export const TemplateEdit: React.FunctionComponent + return ( + - + ); } else if (error) { - content = ( - } - error={error as Error} + error={error} data-test-subj="sectionError" /> ); @@ -91,80 +92,75 @@ export const TemplateEdit: React.FunctionComponent } - color="danger" - iconType="alert" - data-test-subj="systemTemplateEditCallout" - > - - - ); - } else { - content = ( - - {isSystemTemplate && ( - - + error={ + { + message: i18n.translate( + 'xpack.idxMgmt.templateEdit.managedTemplateWarningDescription', + { + defaultMessage: 'Managed templates are critical for internal operations.', } - color="danger" - iconType="alert" - data-test-subj="systemTemplateEditCallout" - > - - - - - )} - -

- -

- - } - defaultValue={template} - onSave={onSave} - isSaving={isSaving} - saveError={saveError} - clearSaveError={clearSaveError} - isEditing={true} - isLegacy={isLegacy} - history={history as ScopedHistory} - /> -
+ ), + } as Error + } + data-test-subj="systemTemplateEditCallout" + /> ); } } return ( - - {content} - + + {isSystemTemplate && ( + + + } + color="danger" + iconType="alert" + data-test-subj="systemTemplateEditCallout" + > + + + + + )} + + + } + defaultValue={template!} + onSave={onSave} + isSaving={isSaving} + saveError={saveError} + clearSaveError={clearSaveError} + isEditing={true} + isLegacy={isLegacy} + history={history as ScopedHistory} + /> + ); }; diff --git a/x-pack/plugins/index_management/public/application/services/use_request.ts b/x-pack/plugins/index_management/public/application/services/use_request.ts index f4d342643956..3b1d5cf22452 100644 --- a/x-pack/plugins/index_management/public/application/services/use_request.ts +++ b/x-pack/plugins/index_management/public/application/services/use_request.ts @@ -11,6 +11,7 @@ import { UseRequestConfig, sendRequest as _sendRequest, useRequest as _useRequest, + Error, } from '../../shared_imports'; import { httpService } from './http'; @@ -19,6 +20,6 @@ export const sendRequest = (config: SendRequestConfig): Promise(config: UseRequestConfig) => { - return _useRequest(httpService.httpClient, config); +export const useRequest = (config: UseRequestConfig) => { + return _useRequest(httpService.httpClient, config); }; diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index eddac8e4b8a8..fa27b22e502f 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -5,6 +5,8 @@ * 2.0. */ +export { APP_WRAPPER_CLASS } from '../../../../src/core/public'; + export { SendRequestConfig, SendRequestResponse, @@ -16,6 +18,10 @@ export { extractQueryParams, GlobalFlyout, attemptToURIDecode, + PageLoading, + PageError, + Error, + SectionLoading, } from '../../../../src/plugins/es_ui_shared/public'; export { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index bd000186d91c..231a2764d271 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -17,28 +17,40 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; -export function registerGetAllRoute({ router }: RouteDependencies) { +export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) { router.get({ path: addBasePath('/index_templates'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; - const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); - const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); - const { index_templates: templatesEs } = await callAsCurrentUser( - 'dataManagement.getComposableIndexTemplates' - ); + try { + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); - const legacyTemplates = deserializeLegacyTemplateList( - legacyTemplatesEs, - cloudManagedTemplatePrefix - ); - const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); + const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); + const { index_templates: templatesEs } = await callAsCurrentUser( + 'dataManagement.getComposableIndexTemplates' + ); - const body = { - templates, - legacyTemplates, - }; + const legacyTemplates = deserializeLegacyTemplateList( + legacyTemplatesEs, + cloudManagedTemplatePrefix + ); + const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); - return res.ok({ body }); + const body = { + templates, + legacyTemplates, + }; + + return res.ok({ body }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + // Case: default + throw error; + } }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3a7d0361f389..89d5ebfc16f0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9795,8 +9795,6 @@ "xpack.idxMgmt.home.componentTemplates.emptyPromptDocumentionLink": "詳細情報", "xpack.idxMgmt.home.componentTemplates.emptyPromptTitle": "コンポーネントテンプレートを作成して開始", "xpack.idxMgmt.home.componentTemplates.list.componentTemplatesDescription": "コンポーネントテンプレートを使用して、複数のインデックステンプレートで設定、マッピング、エイリアス構成を再利用します。{learnMoreLink}", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorReloadLinkLabel": "再試行してください。", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorTitle": "コンポーネントテンプレートを読み込めません。{reloadLink}", "xpack.idxMgmt.home.componentTemplates.list.loadingMessage": "コンポーネントテンプレートを読み込んでいます…", "xpack.idxMgmt.home.componentTemplatesTabTitle": "コンポーネントテンプレート", "xpack.idxMgmt.home.dataStreamsTabTitle": "データストリーム", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5568c3efac34..09ccc43c2c53 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9902,8 +9902,6 @@ "xpack.idxMgmt.home.componentTemplates.emptyPromptDocumentionLink": "了解详情。", "xpack.idxMgmt.home.componentTemplates.emptyPromptTitle": "首先创建组件模板", "xpack.idxMgmt.home.componentTemplates.list.componentTemplatesDescription": "使用组件模板可在多个索引模板中重复使用设置、映射和别名。{learnMoreLink}", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorReloadLinkLabel": "请重试。", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorTitle": "无法加载组件模板。{reloadLink}", "xpack.idxMgmt.home.componentTemplates.list.loadingMessage": "正在加载组件模板……", "xpack.idxMgmt.home.componentTemplatesTabTitle": "组件模板", "xpack.idxMgmt.home.dataStreamsTabTitle": "数据流",