Migrate Index Management to new solutions nav (#101548)

* Migrate index template and component template wizard pages to new nav.
* Convert index templates and component templates pages to new nav.
* Convert indices and data streams pages to new nav.
* Add PageLoading component to es_ui_shared.
* Refactor index table component tests.
* Add missing error reporting to get all templates API route handler.
This commit is contained in:
CJ Cenizal 2021-06-22 17:15:57 -07:00 committed by GitHub
parent e53da4e3eb
commit 5df858aae1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 666 additions and 569 deletions

View file

@ -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<Props> = ({
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 = (
<EuiPageContent verticalPosition="center" horizontalPosition="center" color="danger">
<EuiEmptyPrompt
title={<h2>{title}</h2>}
body={
<>
{cause ? message || errorString : <p>{message || errorString}</p>}
{cause && (
<>
<EuiSpacer size="s" />
<ul>
{cause.map((causeMsg, i) => (
<li key={i}>{causeMsg}</li>
))}
</ul>
</>
)}
</>
error && (
<>
{cause ? message || errorString : <p>{message || errorString}</p>}
{cause && (
<>
<EuiSpacer size="s" />
<ul>
{cause.map((causeMsg, i) => (
<li key={i}>{causeMsg}</li>
))}
</ul>
</>
)}
</>
)
}
iconType="alert"
actions={actions}

View file

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

View file

@ -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 (
<EuiPageContent verticalPosition="center" horizontalPosition="center" color="subdued">
<EuiEmptyPrompt
title={<EuiLoadingSpinner size="xl" />}
body={<EuiText color="subdued">{children}</EuiText>}
data-test-subj="sectionLoading"
/>
</EuiPageContent>
);
};

View file

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

View file

@ -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', () => {
</MemoryRouter>
</Provider>
);
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);
});
});

View file

@ -165,8 +165,10 @@ describe('<ComponentTemplateList />', () => {
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'
);
});
});

View file

@ -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<Props> = ({
}
}, [componentTemplateName, removeContentFromGlobalFlyout]);
let content: React.ReactNode;
if (isLoading) {
content = (
<SectionLoading data-test-subj="sectionLoading">
return (
<PageLoading data-test-subj="sectionLoading">
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.list.loadingMessage"
defaultMessage="Loading component templates…"
/>
</SectionLoading>
</PageLoading>
);
} else if (data?.length) {
}
let content: React.ReactNode;
if (data?.length) {
content = (
<>
<EuiText color="subdued">
@ -183,11 +189,22 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({
} else if (data && data.length === 0) {
content = <EmptyPrompt history={history} />;
} else if (error) {
content = <LoadError onReloadClick={resendRequest} />;
content = (
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.list.loadingErrorMessage"
defaultMessage="Error loading component templates"
/>
}
error={error}
data-test-subj="componentTemplatesLoadError"
/>
);
}
return (
<div data-test-subj="componentTemplateList">
<div className={APP_WRAPPER_CLASS} data-test-subj="componentTemplateList">
{content}
{/* delete modal */}

View file

@ -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<Props> = ({ onReloadClick }) => {
return (
<EuiCallOut
iconType="faceSad"
color="danger"
data-test-subj="componentTemplatesLoadError"
title={
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.list.loadErrorTitle"
defaultMessage="Unable to load component templates. {reloadLink}"
values={{
reloadLink: (
<EuiLink onClick={onReloadClick}>
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.list.loadErrorReloadLinkLabel"
defaultMessage="Try again."
/>
</EuiLink>
),
}}
/>
}
/>
);
};

View file

@ -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 (
<SectionError
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.checkingPrivilegesErrorMessage"
@ -45,12 +45,12 @@ export const ComponentTemplatesWithPrivileges: FunctionComponent = ({
{({ isLoading, hasPrivileges, privilegesMissing }) => {
if (isLoading) {
return (
<SectionLoading>
<PageLoading>
<FormattedMessage
id="xpack.idxMgmt.home.componentTemplates.checkingPrivilegesDescription"
defaultMessage="Checking privileges…"
/>
</SectionLoading>
</PageLoading>
);
}

View file

@ -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<RouteComponentProps<Param
useEffect(() => {
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<RouteComponentProps<Param
if (isLoading) {
return (
<SectionLoading>
<PageLoading>
<FormattedMessage
id="xpack.idxMgmt.componentTemplateEdit.loadingDescription"
defaultMessage="Loading component template…"
/>
</SectionLoading>
</PageLoading>
);
} else {
// We still show the create form (unpopulated) even if we were not able to load the

View file

@ -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<RouteComponentProp
}, [breadcrumbs]);
return (
<EuiPageBody>
<EuiPageContent>
<EuiTitle size="l">
<h1 data-test-subj="pageTitle">
<EuiPageContentBody restrictWidth style={{ width: '100%' }}>
<EuiPageHeader
pageTitle={
<span data-test-subj="pageTitle">
<FormattedMessage
id="xpack.idxMgmt.createComponentTemplate.pageTitle"
defaultMessage="Create component template"
/>
</h1>
</EuiTitle>
</span>
}
bottomBorder
/>
<EuiSpacer size="l" />
<EuiSpacer size="l" />
<ComponentTemplateForm
defaultValue={sourceComponentTemplate}
onSave={onSave}
isSaving={isSaving}
saveError={saveError}
clearSaveError={clearSaveError}
/>
</EuiPageContent>
</EuiPageBody>
<ComponentTemplateForm
defaultValue={sourceComponentTemplate}
onSave={onSave}
isSaving={isSaving}
saveError={saveError}
clearSaveError={clearSaveError}
/>
</EuiPageContentBody>
);
};

View file

@ -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<RouteComponentProps<
setSaveError(null);
};
let content;
if (isLoading) {
content = (
<SectionLoading>
return (
<PageLoading>
<FormattedMessage
id="xpack.idxMgmt.componentTemplateEdit.loadingDescription"
defaultMessage="Loading component template…"
/>
</SectionLoading>
</PageLoading>
);
} else if (error) {
content = (
<>
<EuiCallOut
title={
}
if (error) {
return (
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.componentTemplateEdit.loadComponentTemplateError"
defaultMessage="Error loading component template"
/>
}
error={error as Error}
data-test-subj="loadComponentTemplateError"
/>
);
}
return (
<EuiPageContentBody restrictWidth style={{ width: '100%' }}>
<EuiPageHeader
pageTitle={
<span data-test-subj="pageTitle">
<FormattedMessage
id="xpack.idxMgmt.componentTemplateEdit.loadComponentTemplateError"
defaultMessage="Error loading component template"
id="xpack.idxMgmt.componentTemplateEdit.editPageTitle"
defaultMessage="Edit component template '{name}'"
values={{ name: decodedName }}
/>
}
color="danger"
iconType="alert"
data-test-subj="loadComponentTemplateError"
>
<div>{error.message}</div>
</EuiCallOut>
<EuiSpacer size="m" />
</>
);
} else if (componentTemplate) {
content = (
</span>
}
bottomBorder
/>
<EuiSpacer size="l" />
<ComponentTemplateForm
defaultValue={componentTemplate}
defaultValue={componentTemplate!}
onSave={onSave}
isSaving={isSaving}
saveError={saveError}
clearSaveError={clearSaveError}
isEditing={true}
/>
);
}
return (
<EuiPageBody>
<EuiPageContent>
<EuiTitle size="l">
<h1 data-test-subj="pageTitle">
<FormattedMessage
id="xpack.idxMgmt.componentTemplateEdit.editPageTitle"
defaultMessage="Edit component template '{name}'"
values={{ name: decodedName }}
/>
</h1>
</EuiTitle>
<EuiSpacer size="l" />
{content}
</EuiPageContent>
</EuiPageBody>
</EuiPageContentBody>
);
};

View file

@ -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<ComponentTemplateListItem[], Error>({
return useRequest<ComponentTemplateListItem[]>({
path: `${apiBasePath}/component_templates`,
method: 'get',
});

View file

@ -14,6 +14,7 @@ import {
SendRequestResponse,
sendRequest as _sendRequest,
useRequest as _useRequest,
Error,
} from '../shared_imports';
export type UseRequestHook = <T = any, E = Error>(

View file

@ -12,10 +12,12 @@ export {
SendRequestResponse,
sendRequest,
useRequest,
SectionLoading,
WithPrivileges,
AuthorizationProvider,
SectionError,
SectionLoading,
PageLoading,
PageError,
Error,
useAuthorizationContext,
NotAuthorizedSection,

View file

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

View file

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

View file

@ -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 (
<EuiPageContent>
<EuiEmptyPrompt
iconType="securityApp"
iconColor={undefined}
title={
<h1>
<FormattedMessage
id="xpack.idxMgmt.pageErrorForbidden.title"
defaultMessage="You do not have permissions to use Index Management"
/>
</h1>
}
/>
</EuiPageContent>
);
}

View file

@ -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<Props> = ({ children }) => {
return (
<EuiEmptyPrompt
title={<EuiLoadingSpinner size="xl" />}
body={<EuiText color="subdued">{children}</EuiText>}
data-test-subj="sectionLoading"
/>
);
};

View file

@ -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}
<EuiPageHeader pageTitle={<span data-test-subj="pageTitle">{title}</span>} bottomBorder />
<EuiSpacer size="m" />

View file

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

View file

@ -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<RouteComponentProps<MatchPa
if (isLoading) {
content = (
<SectionLoading>
<PageLoading>
<FormattedMessage
id="xpack.idxMgmt.dataStreamList.loadingDataStreamsDescription"
defaultMessage="Loading data streams…"
/>
</SectionLoading>
</PageLoading>
);
} else if (error) {
content = (
<SectionError
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.dataStreamList.loadingDataStreamsErrorMessage"
@ -252,10 +256,10 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
data-test-subj="emptyPrompt"
/>
);
} else if (Array.isArray(dataStreams) && dataStreams.length > 0) {
activateHiddenFilter(isSelectedDataStreamHidden(dataStreams, decodedDataStreamName));
} else {
activateHiddenFilter(isSelectedDataStreamHidden(dataStreams!, decodedDataStreamName));
content = (
<>
<EuiPageContent hasShadow={false} paddingSize="none" data-test-subj="dataStreamList">
{renderHeader()}
<EuiSpacer size="l" />
@ -270,12 +274,12 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa
history={history as ScopedHistory}
includeStats={isIncludeStatsChecked}
/>
</>
</EuiPageContent>
);
}
return (
<div data-test-subj="dataStreamList">
<div className={APP_WRAPPER_CLASS}>
{content}
{/*

View file

@ -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<RouteComponentProps> = ({ history }) => {
return (
<div className="im-snapshotTestSubject" data-test-subj="indicesList">
<div className={`${APP_WRAPPER_CLASS} im-snapshotTestSubject`} data-test-subj="indicesList">
<IndexTable history={history} />
<DetailPanel />
</div>

View file

@ -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 (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.idxMgmt.indexTable.serverErrorTitle"
defaultMessage="Error loading indices"
/>
}
color="danger"
iconType="alert"
>
<div>{message || errorString}</div>
{cause && (
<Fragment>
<EuiSpacer size="m" />
<ul>
{cause.map((message, i) => (
<li key={i}>{message}</li>
))}
</ul>
</Fragment>
)}
</EuiCallOut>
<EuiSpacer size="xl" />
</Fragment>
);
}
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 (
<PageLoading>
<FormattedMessage
id="xpack.idxMgmt.indexTable.loadingIndicesDescription"
defaultMessage="Loading indices…"
/>
</PageLoading>
);
}
if (indicesLoading) {
emptyState = (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
if (indicesError) {
if (indicesError.status === 403) {
return (
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.pageErrorForbidden.title"
defaultMessage="You do not have permissions to use Index Management"
/>
}
/>
);
}
return (
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.indexTable.serverErrorTitle"
defaultMessage="Error loading indices"
/>
}
error={indicesError.body}
/>
);
}
};
return (
<EuiPageContent
hasShadow={false}
paddingSize="none"
verticalPosition="center"
horizontalPosition="center"
>
{renderNoContent()}
</EuiPageContent>
);
}
if (!indicesLoading && !indicesError) {
emptyState = <NoMatch />;
}
const { selectedIndicesMap } = this.state;
const atLeastOneItemSelected = Object.keys(selectedIndicesMap).length > 0;
if (indicesError && indicesError.status === 403) {
return <PageErrorForbidden />;
}
return (
<AppContextConsumer>
{({ services }) => {
const { extensionsService } = services;
return (
<Fragment>
<EuiPageContent hasShadow={false} paddingSize="none">
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={true}>
<EuiText color="subdued">
@ -557,8 +560,6 @@ export class IndexTable extends Component {
{this.renderBanners(extensionsService)}
{indicesError && this.renderError()}
<EuiFlexGroup gutterSize="l" alignItems="center">
{atLeastOneItemSelected ? (
<EuiFlexItem grow={false}>
@ -665,13 +666,13 @@ export class IndexTable extends Component {
</EuiTable>
</div>
) : (
emptyState
<NoMatch />
)}
<EuiSpacer size="m" />
{indices.length > 0 ? this.renderPager() : null}
</Fragment>
</EuiPageContent>
);
}}
</AppContextConsumer>

View file

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

View file

@ -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<RouteComponentProps<MatchPara
};
const renderHeader = () => (
<EuiFlexGroup alignItems="center" gutterSize="s">
// flex-grow: 0 is needed here because the parent element is a flex column and the header would otherwise expand.
<EuiFlexGroup alignItems="center" gutterSize="s" style={{ flexGrow: 0 }}>
<EuiFlexItem grow={true}>
<EuiText color="subdued">
<FormattedMessage
@ -218,77 +220,99 @@ export const TemplateList: React.FunctionComponent<RouteComponentProps<MatchPara
</>
);
const renderContent = () => {
if (isLoading) {
return (
<SectionLoading>
<FormattedMessage
id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription"
defaultMessage="Loading templates…"
/>
</SectionLoading>
);
} else if (error) {
return (
<SectionError
title={
<FormattedMessage
id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage"
defaultMessage="Error loading templates"
/>
}
error={error as Error}
/>
);
} else if (!hasTemplates) {
return (
<EuiEmptyPrompt
iconType="managementApp"
title={
<h1 data-test-subj="title">
<FormattedMessage
id="xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle"
defaultMessage="You don't have any templates yet"
/>
</h1>
}
data-test-subj="emptyPrompt"
/>
);
} else {
return (
<Fragment>
{/* 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()}
</Fragment>
);
}
};
// Track component loaded
// Track this component mounted.
useEffect(() => {
uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD);
}, [uiMetricService]);
return (
<div data-test-subj="templateList">
{renderContent()}
let content;
{isTemplateDetailsVisible && (
<TemplateDetails
template={selectedTemplate!}
onClose={closeTemplateDetails}
editTemplate={editTemplate}
cloneTemplate={cloneTemplate}
reload={reload}
if (isLoading) {
content = (
<PageLoading>
<FormattedMessage
id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription"
defaultMessage="Loading templates…"
/>
)}
</PageLoading>
);
} else if (error) {
content = (
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage"
defaultMessage="Error loading templates"
/>
}
error={error}
/>
);
} else if (!hasTemplates) {
content = (
<EuiEmptyPrompt
iconType="managementApp"
title={
<h1 data-test-subj="title">
<FormattedMessage
id="xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle"
defaultMessage="Create your first index template"
/>
</h1>
}
body={
<>
<p>
<FormattedMessage
id="xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesDescription"
defaultMessage="An index template automatically applies settings, mappings, and aliases to new indices."
/>
</p>
</>
}
actions={
<EuiButton
{...reactRouterNavigate(history, '/create_template')}
fill
iconType="plusInCircle"
>
<FormattedMessage
id="xpack.idxMgmt.indexTemplatesList.emptyPrompt.createTemplatesButtonLabel"
defaultMessage="Create template"
/>
</EuiButton>
}
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 && (
<TemplateDetails
template={selectedTemplate!}
onClose={closeTemplateDetails}
editTemplate={editTemplate}
cloneTemplate={cloneTemplate}
reload={reload}
/>
)}
</>
);
}
return (
<div data-test-subj="templateList" className={APP_WRAPPER_CLASS}>
{content}
</div>
);
};

View file

@ -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<RouteComponentProps<MatchPar
setSaveError(null);
};
let content;
useEffect(() => {
breadcrumbService.setBreadcrumbs('templateClone');
}, []);
if (isLoading) {
content = (
<SectionLoading>
return (
<PageLoading>
<FormattedMessage
id="xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription"
defaultMessage="Loading template to clone…"
/>
</SectionLoading>
</PageLoading>
);
} else if (templateToCloneError) {
content = (
<SectionError
return (
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage"
@ -90,24 +89,22 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar
data-test-subj="sectionError"
/>
);
} else if (templateToClone) {
const templateData = {
...templateToClone,
name: `${decodedTemplateName}-copy`,
} as TemplateDeserialized;
}
content = (
const templateData = {
...templateToClone,
name: `${decodedTemplateName}-copy`,
} as TemplateDeserialized;
return (
<EuiPageContentBody restrictWidth style={{ width: '100%' }}>
<TemplateForm
title={
<EuiTitle size="l">
<h1 data-test-subj="pageTitle">
<FormattedMessage
id="xpack.idxMgmt.createTemplate.cloneTemplatePageTitle"
defaultMessage="Clone template '{name}'"
values={{ name: decodedTemplateName }}
/>
</h1>
</EuiTitle>
<FormattedMessage
id="xpack.idxMgmt.createTemplate.cloneTemplatePageTitle"
defaultMessage="Clone template '{name}'"
values={{ name: decodedTemplateName }}
/>
}
defaultValue={templateData}
onSave={onSave}
@ -117,12 +114,6 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar
isLegacy={isLegacy}
history={history as ScopedHistory}
/>
);
}
return (
<EuiPageBody>
<EuiPageContent>{content}</EuiPageContent>
</EuiPageBody>
</EuiPageContentBody>
);
};

View file

@ -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<RouteComponentProps> = ({ h
}, []);
return (
<EuiPageBody>
<EuiPageContent>
<TemplateForm
title={
<EuiTitle size="l">
<h1 data-test-subj="pageTitle">
{isLegacy ? (
<FormattedMessage
id="xpack.idxMgmt.createTemplate.createLegacyTemplatePageTitle"
defaultMessage="Create legacy template"
/>
) : (
<FormattedMessage
id="xpack.idxMgmt.createTemplate.createTemplatePageTitle"
defaultMessage="Create template"
/>
)}
</h1>
</EuiTitle>
}
onSave={onSave}
isSaving={isSaving}
saveError={saveError}
clearSaveError={clearSaveError}
isLegacy={isLegacy}
history={history as ScopedHistory}
/>
</EuiPageContent>
</EuiPageBody>
<EuiPageContentBody restrictWidth style={{ width: '100%' }}>
<TemplateForm
title={
isLegacy ? (
<FormattedMessage
id="xpack.idxMgmt.createTemplate.createLegacyTemplatePageTitle"
defaultMessage="Create legacy template"
/>
) : (
<FormattedMessage
id="xpack.idxMgmt.createTemplate.createTemplatePageTitle"
defaultMessage="Create template"
/>
)
}
onSave={onSave}
isSaving={isSaving}
saveError={saveError}
clearSaveError={clearSaveError}
isLegacy={isLegacy}
history={history as ScopedHistory}
/>
</EuiPageContentBody>
);
};

View file

@ -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<RouteComponentProps<MatchPara
setSaveError(null);
};
let content;
let isSystemTemplate;
if (isLoading) {
content = (
<SectionLoading>
return (
<PageLoading>
<FormattedMessage
id="xpack.idxMgmt.templateEdit.loadingIndexTemplateDescription"
defaultMessage="Loading template…"
/>
</SectionLoading>
</PageLoading>
);
} else if (error) {
content = (
<SectionError
return (
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.templateEdit.loadingIndexTemplateErrorMessage"
defaultMessage="Error loading template"
/>
}
error={error as Error}
error={error}
data-test-subj="sectionError"
/>
);
@ -91,80 +92,75 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
name: templateName,
_kbnMeta: { type },
} = template;
const isSystemTemplate = templateName && templateName.startsWith('.');
isSystemTemplate = templateName && templateName.startsWith('.');
if (type === 'cloudManaged') {
content = (
<EuiCallOut
return (
<PageError
title={
<FormattedMessage
id="xpack.idxMgmt.templateEdit.managedTemplateWarningTitle"
defaultMessage="Editing a managed template is not permitted"
/>
}
color="danger"
iconType="alert"
data-test-subj="systemTemplateEditCallout"
>
<FormattedMessage
id="xpack.idxMgmt.templateEdit.managedTemplateWarningDescription"
defaultMessage="Managed templates are critical for internal operations."
/>
</EuiCallOut>
);
} else {
content = (
<Fragment>
{isSystemTemplate && (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.idxMgmt.templateEdit.systemTemplateWarningTitle"
defaultMessage="Editing a system template can break Kibana"
/>
error={
{
message: i18n.translate(
'xpack.idxMgmt.templateEdit.managedTemplateWarningDescription',
{
defaultMessage: 'Managed templates are critical for internal operations.',
}
color="danger"
iconType="alert"
data-test-subj="systemTemplateEditCallout"
>
<FormattedMessage
id="xpack.idxMgmt.templateEdit.systemTemplateWarningDescription"
defaultMessage="System templates are critical for internal operations."
/>
</EuiCallOut>
<EuiSpacer size="l" />
</Fragment>
)}
<TemplateForm
title={
<EuiTitle size="l">
<h1 data-test-subj="pageTitle">
<FormattedMessage
id="xpack.idxMgmt.editTemplate.editTemplatePageTitle"
defaultMessage="Edit template '{name}'"
values={{ name: decodedTemplateName }}
/>
</h1>
</EuiTitle>
}
defaultValue={template}
onSave={onSave}
isSaving={isSaving}
saveError={saveError}
clearSaveError={clearSaveError}
isEditing={true}
isLegacy={isLegacy}
history={history as ScopedHistory}
/>
</Fragment>
),
} as Error
}
data-test-subj="systemTemplateEditCallout"
/>
);
}
}
return (
<EuiPageBody>
<EuiPageContent>{content}</EuiPageContent>
</EuiPageBody>
<EuiPageContentBody restrictWidth style={{ width: '100%' }}>
{isSystemTemplate && (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.idxMgmt.templateEdit.systemTemplateWarningTitle"
defaultMessage="Editing a system template can break Kibana"
/>
}
color="danger"
iconType="alert"
data-test-subj="systemTemplateEditCallout"
>
<FormattedMessage
id="xpack.idxMgmt.templateEdit.systemTemplateWarningDescription"
defaultMessage="System templates are critical for internal operations."
/>
</EuiCallOut>
<EuiSpacer size="l" />
</Fragment>
)}
<TemplateForm
title={
<FormattedMessage
id="xpack.idxMgmt.editTemplate.editTemplatePageTitle"
defaultMessage="Edit template '{name}'"
values={{ name: decodedTemplateName }}
/>
}
defaultValue={template!}
onSave={onSave}
isSaving={isSaving}
saveError={saveError}
clearSaveError={clearSaveError}
isEditing={true}
isLegacy={isLegacy}
history={history as ScopedHistory}
/>
</EuiPageContentBody>
);
};

View file

@ -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<SendRequestRespo
return _sendRequest(httpService.httpClient, config);
};
export const useRequest = <T = any>(config: UseRequestConfig) => {
return _useRequest<T>(httpService.httpClient, config);
export const useRequest = <T = any, E = Error>(config: UseRequestConfig) => {
return _useRequest<T, E>(httpService.httpClient, config);
};

View file

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

View file

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

View file

@ -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": "データストリーム",

View file

@ -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": "数据流",