diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/constants.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/constants.ts
new file mode 100644
index 0000000000000..db29ed844b606
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/constants.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const TEMPLATE_NAME = 'my_template';
+
+export const INDEX_PATTERNS = ['my_index_pattern'];
+
+export const SETTINGS = {
+ number_of_shards: 1,
+ index: {
+ lifecycle: {
+ name: 'my_policy',
+ },
+ },
+};
+
+export const ALIASES = {
+ alias: {
+ filter: {
+ term: { user: 'my_user' },
+ },
+ },
+};
+
+export const MAPPINGS = {
+ _source: {
+ enabled: false,
+ },
+ properties: {
+ host_name: {
+ type: 'keyword',
+ },
+ created_at: {
+ type: 'date',
+ format: 'EEE MMM dd HH:mm:ss Z yyyy',
+ },
+ },
+};
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts
index d2857d5f5f54b..eed13fb298465 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import {
registerTestBed,
@@ -15,6 +16,7 @@ import {
import { IndexManagementHome } from '../../../public/sections/home';
import { BASE_PATH } from '../../../common/constants';
import { indexManagementStore } from '../../../public/store';
+import { Template } from '../../../common/types';
const testBedConfig: TestBedConfig = {
store: indexManagementStore,
@@ -28,13 +30,15 @@ const testBedConfig: TestBedConfig = {
const initTestBed = registerTestBed(IndexManagementHome, testBedConfig);
export interface IdxMgmtHomeTestBed extends TestBed {
+ findAction: (action: 'edit' | 'clone' | 'delete') => ReactWrapper;
actions: {
- selectHomeTab: (tab: 'indices' | 'index templates') => void;
+ selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void;
selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void;
clickReloadButton: () => void;
- clickTemplateActionAt: (index: number, action: 'delete') => void;
+ clickTemplateAction: (name: Template['name'], action: 'edit' | 'clone' | 'delete') => void;
clickTemplateAt: (index: number) => void;
clickCloseDetailsButton: () => void;
+ clickActionMenu: (name: Template['name']) => void;
};
}
@@ -42,16 +46,21 @@ export const setup = async (): Promise => {
const testBed = await initTestBed();
/**
- * User Actions
+ * Additional helpers
*/
+ const findAction = (action: 'edit' | 'clone' | 'delete') => {
+ const actions = ['edit', 'clone', 'delete'];
+ const { component } = testBed;
- const selectHomeTab = (tab: 'indices' | 'index templates') => {
- const tabs = ['indices', 'index templates'];
+ return component.find('.euiContextMenuItem').at(actions.indexOf(action));
+ };
- testBed
- .find('tab')
- .at(tabs.indexOf(tab))
- .simulate('click');
+ /**
+ * User Actions
+ */
+
+ const selectHomeTab = (tab: 'indicesTab' | 'templatesTab') => {
+ testBed.find(tab).simulate('click');
};
const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => {
@@ -68,23 +77,32 @@ export const setup = async (): Promise => {
find('reloadButton').simulate('click');
};
- const clickTemplateActionAt = async (index: number, action: 'delete') => {
- const { component, table } = testBed;
- const { rows } = table.getMetaData('templatesTable');
- const currentRow = rows[index];
- const lastColumn = currentRow.columns[currentRow.columns.length - 1].reactWrapper;
- const button = findTestSubject(lastColumn, `${action}TemplateButton`);
+ const clickActionMenu = async (templateName: Template['name']) => {
+ const { component } = testBed;
- // @ts-ignore (remove when react 16.9.0 is released)
- await act(async () => {
- button.simulate('click');
- component.update();
- });
+ // When a table has > 2 actions, EUI displays an overflow menu with an id "-actions"
+ // The template name may contain a period (.) so we use bracket syntax for selector
+ component.find(`div[id="${templateName}-actions"] button`).simulate('click');
+ };
+
+ const clickTemplateAction = (
+ templateName: Template['name'],
+ action: 'edit' | 'clone' | 'delete'
+ ) => {
+ const actions = ['edit', 'clone', 'delete'];
+ const { component } = testBed;
+
+ clickActionMenu(templateName);
+
+ component
+ .find('.euiContextMenuItem')
+ .at(actions.indexOf(action))
+ .simulate('click');
};
const clickTemplateAt = async (index: number) => {
const { component, table, router } = testBed;
- const { rows } = table.getMetaData('templatesTable');
+ const { rows } = table.getMetaData('templateTable');
const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink');
// @ts-ignore (remove when react 16.9.0 is released)
@@ -104,13 +122,15 @@ export const setup = async (): Promise => {
return {
...testBed,
+ findAction,
actions: {
selectHomeTab,
selectDetailsTab,
clickReloadButton,
- clickTemplateActionAt,
+ clickTemplateAction,
clickTemplateAt,
clickCloseDetailsButton,
+ clickActionMenu,
},
};
};
@@ -122,14 +142,19 @@ export type TestSubjects =
| 'appTitle'
| 'cell'
| 'closeDetailsButton'
+ | 'createTemplateButton'
| 'deleteSystemTemplateCallOut'
| 'deleteTemplateButton'
- | 'deleteTemplatesButton'
| 'deleteTemplatesConfirmation'
| 'documentationLink'
| 'emptyPrompt'
+ | 'manageTemplateButton'
| 'mappingsTab'
+ | 'noAliasesCallout'
+ | 'noMappingsCallout'
+ | 'noSettingsCallout'
| 'indicesList'
+ | 'indicesTab'
| 'reloadButton'
| 'row'
| 'sectionError'
@@ -138,11 +163,11 @@ export type TestSubjects =
| 'summaryTab'
| 'summaryTitle'
| 'systemTemplatesSwitch'
- | 'tab'
| 'templateDetails'
- | 'templateDetails.deleteTemplateButton'
+ | 'templateDetails.manageTemplateButton'
| 'templateDetails.sectionLoading'
| 'templateDetails.tab'
| 'templateDetails.title'
- | 'templatesList'
- | 'templatesTable';
+ | 'templateList'
+ | 'templateTable'
+ | 'templatesTab';
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
index 90320740d09ae..906b384dddc70 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
@@ -47,11 +47,35 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
+ const setCreateTemplateResponse = (response?: HttpResponse, error?: any) => {
+ const status = error ? error.status || 400 : 200;
+ const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
+
+ server.respondWith('PUT', `${API_PATH}/templates`, [
+ status,
+ { 'Content-Type': 'application/json' },
+ body,
+ ]);
+ };
+
+ const setUpdateTemplateResponse = (response?: HttpResponse, error?: any) => {
+ const status = error ? error.status || 400 : 200;
+ const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
+
+ server.respondWith('PUT', `${API_PATH}/templates/:name`, [
+ status,
+ { 'Content-Type': 'application/json' },
+ body,
+ ]);
+ };
+
return {
setLoadTemplatesResponse,
setLoadIndicesResponse,
setDeleteTemplateResponse,
setLoadTemplateResponse,
+ setCreateTemplateResponse,
+ setUpdateTemplateResponse,
};
};
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/index.ts
index ec4af7c0df417..6dce4453a67f9 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/index.ts
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/index.ts
@@ -5,6 +5,9 @@
*/
import { setup as homeSetup } from './home.helpers';
+import { setup as templateCreateSetup } from './template_create.helpers';
+import { setup as templateCloneSetup } from './template_clone.helpers';
+import { setup as templateEditSetup } from './template_edit.helpers';
export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../../test_utils';
@@ -12,4 +15,7 @@ export { setupEnvironment } from './setup_environment';
export const pageHelpers = {
home: { setup: homeSetup },
+ templateCreate: { setup: templateCreateSetup },
+ templateClone: { setup: templateCloneSetup },
+ templateEdit: { setup: templateEditSetup },
};
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts
new file mode 100644
index 0000000000000..1401ea7c04906
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
+import { BASE_PATH } from '../../../common/constants';
+import { TemplateClone } from '../../../public/sections/template_clone';
+import { formSetup } from './template_form.helpers';
+import { TEMPLATE_NAME } from './constants';
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [`${BASE_PATH}clone_template/${TEMPLATE_NAME}`],
+ componentRoutePath: `${BASE_PATH}clone_template/:name`,
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(TemplateClone, testBedConfig);
+
+export const setup = formSetup.bind(null, initTestBed);
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts
new file mode 100644
index 0000000000000..67c429185ac9b
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
+import { BASE_PATH } from '../../../common/constants';
+import { TemplateCreate } from '../../../public/sections/template_create';
+import { formSetup } from './template_form.helpers';
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [`${BASE_PATH}create_template`],
+ componentRoutePath: `${BASE_PATH}create_template`,
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(TemplateCreate, testBedConfig);
+
+export const setup = formSetup.bind(null, initTestBed);
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts
new file mode 100644
index 0000000000000..9976b395376b4
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
+import { BASE_PATH } from '../../../common/constants';
+import { TemplateEdit } from '../../../public/sections/template_edit';
+import { formSetup } from './template_form.helpers';
+import { TEMPLATE_NAME } from './constants';
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [`${BASE_PATH}edit_template/${TEMPLATE_NAME}`],
+ componentRoutePath: `${BASE_PATH}edit_template/:name`,
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(TemplateEdit, testBedConfig);
+
+export const setup = formSetup.bind(null, initTestBed);
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
new file mode 100644
index 0000000000000..84b1f4a77e226
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
@@ -0,0 +1,156 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TestBed } from '../../../../../../test_utils';
+import { Template } from '../../../common/types';
+
+export interface TemplateFormTestBed extends TestBed {
+ actions: {
+ clickNextButton: () => void;
+ clickBackButton: () => void;
+ clickSubmitButton: () => void;
+ completeStepOne: ({ name, indexPatterns, order, version }: Partial) => void;
+ completeStepTwo: ({ settings }: Partial) => void;
+ completeStepThree: ({ mappings }: Partial) => void;
+ completeStepFour: ({ aliases }: Partial) => void;
+ selectSummaryTab: (tab: 'summary' | 'request') => void;
+ };
+}
+
+export const formSetup = async (initTestBed: any): Promise => {
+ const testBed = await initTestBed();
+
+ // User actions
+ const clickNextButton = () => {
+ testBed.find('nextButton').simulate('click');
+ };
+
+ const clickBackButton = () => {
+ testBed.find('backButton').simulate('click');
+ };
+
+ const clickSubmitButton = () => {
+ testBed.find('submitButton').simulate('click');
+ };
+
+ const completeStepOne = ({ name, indexPatterns, order, version }: Partial) => {
+ const { form, find } = testBed;
+
+ if (name) {
+ form.setInputValue('nameInput', name);
+ }
+
+ if (indexPatterns) {
+ const indexPatternsFormatted = indexPatterns.map((pattern: string) => ({
+ label: pattern,
+ value: pattern,
+ }));
+
+ find('mockComboBox').simulate('change', indexPatternsFormatted); // Using mocked EuiComboBox
+ }
+
+ if (order) {
+ form.setInputValue('orderInput', JSON.stringify(order));
+ }
+
+ if (version) {
+ form.setInputValue('versionInput', JSON.stringify(version));
+ }
+
+ clickNextButton();
+ };
+
+ const completeStepTwo = ({ settings }: Partial) => {
+ const { find } = testBed;
+
+ if (settings) {
+ find('mockCodeEditor').simulate('change', {
+ jsonString: settings,
+ }); // Using mocked EuiCodeEditor
+ }
+
+ clickNextButton();
+ };
+
+ const completeStepThree = ({ mappings }: Partial) => {
+ const { find } = testBed;
+
+ if (mappings) {
+ find('mockCodeEditor').simulate('change', {
+ jsonString: mappings,
+ }); // Using mocked EuiCodeEditor
+ }
+
+ clickNextButton();
+ };
+
+ const completeStepFour = ({ aliases }: Partial) => {
+ const { find } = testBed;
+
+ if (aliases) {
+ find('mockCodeEditor').simulate('change', {
+ jsonString: aliases,
+ }); // Using mocked EuiCodeEditor
+ }
+
+ clickNextButton();
+ };
+
+ const selectSummaryTab = (tab: 'summary' | 'request') => {
+ const tabs = ['summary', 'request'];
+
+ testBed
+ .find('summaryTabContent')
+ .find('.euiTab')
+ .at(tabs.indexOf(tab))
+ .simulate('click');
+ };
+
+ return {
+ ...testBed,
+ actions: {
+ clickNextButton,
+ clickBackButton,
+ clickSubmitButton,
+ completeStepOne,
+ completeStepTwo,
+ completeStepThree,
+ completeStepFour,
+ selectSummaryTab,
+ },
+ };
+};
+
+export type TemplateFormTestSubjects = TestSubjects;
+
+type TestSubjects =
+ | 'backButton'
+ | 'codeEditorContainer'
+ | 'indexPatternsComboBox'
+ | 'indexPatternsWarning'
+ | 'indexPatternsWarningDescription'
+ | 'mockCodeEditor'
+ | 'mockComboBox'
+ | 'nameInput'
+ | 'nextButton'
+ | 'orderInput'
+ | 'pageTitle'
+ | 'requestTab'
+ | 'saveTemplateError'
+ | 'settingsEditor'
+ | 'systemTemplateEditCallout'
+ | 'stepAliases'
+ | 'stepMappings'
+ | 'stepSettings'
+ | 'stepSummary'
+ | 'stepTitle'
+ | 'submitButton'
+ | 'summaryTab'
+ | 'summaryTabContent'
+ | 'templateForm'
+ | 'templateFormContainer'
+ | 'testingEditor'
+ | 'versionInput';
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts
index fbd0f6419979b..fb297717b3508 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts
@@ -6,13 +6,7 @@
import { act } from 'react-dom/test-utils';
import * as fixtures from '../../test/fixtures';
-import {
- setupEnvironment,
- pageHelpers,
- nextTick,
- getRandomString,
- findTestSubject,
-} from './helpers';
+import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
import { IdxMgmtHomeTestBed } from './helpers/home.helpers';
const API_PATH = '/api/index_management';
@@ -27,6 +21,17 @@ const removeWhiteSpaceOnArrayValues = (array: any[]) =>
return value.trim();
});
+jest.mock('ui/index_patterns', () => ({
+ ILLEGAL_CHARACTERS: '',
+ CONTAINS_SPACES: '',
+ validateIndexPattern: () => {},
+}));
+
+jest.mock('ui/chrome', () => ({
+ breadcrumbs: { set: () => {} },
+ addBasePath: (path: string) => path || '/api/index_management',
+}));
+
// We need to skip the tests until react 16.9.0 is released
// which supports asynchronous code inside act()
describe.skip('', () => {
@@ -67,20 +72,24 @@ describe.skip('', () => {
describe('tabs', () => {
test('should have 2 tabs', () => {
const { find } = testBed;
+ const templatesTab = find('templatesTab');
+ const indicesTab = find('indicesTab');
- expect(find('tab').length).toBe(2);
- expect(find('tab').map(t => t.text())).toEqual(['Indices', 'Index Templates']);
+ expect(indicesTab.length).toBe(1);
+ expect(indicesTab.text()).toEqual('Indices');
+ expect(templatesTab.length).toBe(1);
+ expect(templatesTab.text()).toEqual('Index Templates');
});
test('should navigate to Index Templates tab', async () => {
const { exists, actions, component } = testBed;
expect(exists('indicesList')).toBe(true);
- expect(exists('templatesList')).toBe(false);
+ expect(exists('templateList')).toBe(false);
httpRequestsMockHelpers.setLoadTemplatesResponse([]);
- actions.selectHomeTab('index templates');
+ actions.selectHomeTab('templatesTab');
// @ts-ignore (remove when react 16.9.0 is released)
await act(async () => {
@@ -89,7 +98,7 @@ describe.skip('', () => {
});
expect(exists('indicesList')).toBe(false);
- expect(exists('templatesList')).toBe(true);
+ expect(exists('templateList')).toBe(true);
});
});
@@ -100,7 +109,7 @@ describe.skip('', () => {
httpRequestsMockHelpers.setLoadTemplatesResponse([]);
- actions.selectHomeTab('index templates');
+ actions.selectHomeTab('templatesTab');
// @ts-ignore (remove when react 16.9.0 is released)
await act(async () => {
@@ -121,14 +130,14 @@ describe.skip('', () => {
const template1 = fixtures.getTemplate({
name: `a${getRandomString()}`,
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
- settings: {
+ settings: JSON.stringify({
index: {
number_of_shards: '1',
lifecycle: {
name: 'my_ilm_policy',
},
},
- },
+ }),
});
const template2 = fixtures.getTemplate({
name: `b${getRandomString()}`,
@@ -146,7 +155,7 @@ describe.skip('', () => {
httpRequestsMockHelpers.setLoadTemplatesResponse(templates);
- actions.selectHomeTab('index templates');
+ actions.selectHomeTab('templatesTab');
// @ts-ignore (remove when react 16.9.0 is released)
await act(async () => {
@@ -158,22 +167,21 @@ describe.skip('', () => {
test('should list them in the table', async () => {
const { table } = testBed;
- const { tableCellsValues } = table.getMetaData('templatesTable');
+ const { tableCellsValues } = table.getMetaData('templateTable');
tableCellsValues.forEach((row, i) => {
const template = templates[i];
- const { name, indexPatterns, order, settings } = template;
- const ilmPolicyName =
- settings && settings.index && settings.index.lifecycle
- ? settings.index.lifecycle.name
- : '';
+ const { name, indexPatterns, order, ilmPolicy } = template;
+
+ const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : '';
+ const orderFormatted = order ? order.toString() : order;
expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
'',
name,
indexPatterns.join(', '),
ilmPolicyName,
- order.toString(),
+ orderFormatted,
'',
'',
'',
@@ -199,9 +207,14 @@ describe.skip('', () => {
expect(server.requests[server.requests.length - 1].url).toBe(`${API_PATH}/templates`);
});
+ test('should have a button to create a new template', () => {
+ const { exists } = testBed;
+ expect(exists('createTemplateButton')).toBe(true);
+ });
+
test('should have a switch to view system templates', async () => {
const { table, exists, component, form } = testBed;
- const { rows } = table.getMetaData('templatesTable');
+ const { rows } = table.getMetaData('templateTable');
expect(rows.length).toEqual(
templates.filter(template => !template.name.startsWith('.')).length
@@ -216,33 +229,59 @@ describe.skip('', () => {
component.update();
});
- const { rows: updatedRows } = table.getMetaData('templatesTable');
+ const { rows: updatedRows } = table.getMetaData('templateTable');
expect(updatedRows.length).toEqual(templates.length);
});
- test('each row should have a link to the template', async () => {
+ test('each row should have a link to the template details panel', async () => {
const { find, exists, actions } = testBed;
await actions.clickTemplateAt(0);
- expect(exists('templatesList')).toBe(true);
+ expect(exists('templateList')).toBe(true);
expect(exists('templateDetails')).toBe(true);
expect(find('templateDetails.title').text()).toBe(template1.name);
});
- describe('delete index template', () => {
- test('should have action buttons on each row to delete an index template', () => {
- const { table } = testBed;
- const { rows } = table.getMetaData('templatesTable');
- const lastColumn = rows[0].columns[rows[0].columns.length - 1].reactWrapper;
+ test('template actions column should have an option to delete', () => {
+ const { actions, findAction } = testBed;
+ const { name: templateName } = template1;
- expect(findTestSubject(lastColumn, 'deleteTemplateButton').length).toBe(1);
- });
+ actions.clickActionMenu(templateName);
+ const deleteAction = findAction('delete');
+
+ expect(deleteAction.text()).toEqual('Delete');
+ });
+
+ test('template actions column should have an option to clone', () => {
+ const { actions, findAction } = testBed;
+ const { name: templateName } = template1;
+
+ actions.clickActionMenu(templateName);
+
+ const cloneAction = findAction('clone');
+
+ expect(cloneAction.text()).toEqual('Clone');
+ });
+
+ test('template actions column should have an option to edit', () => {
+ const { actions, findAction } = testBed;
+ const { name: templateName } = template1;
+
+ actions.clickActionMenu(templateName);
+
+ const editAction = findAction('edit');
+
+ expect(editAction.text()).toEqual('Edit');
+ });
+
+ describe('delete index template', () => {
test('should show a confirmation when clicking the delete template button', async () => {
const { actions } = testBed;
+ const { name: templateName } = template1;
- await actions.clickTemplateActionAt(0, 'delete');
+ await actions.clickTemplateAction(templateName, 'delete');
// We need to read the document "body" as the modal is added there and not inside
// the component DOM tree.
@@ -266,7 +305,8 @@ describe.skip('', () => {
component.update();
});
- await actions.clickTemplateActionAt(0, 'delete');
+ const { name: systemTemplateName } = template3;
+ await actions.clickTemplateAction(systemTemplateName, 'delete');
expect(
document.body.querySelector('[data-test-subj="deleteSystemTemplateCallOut"]')
@@ -275,11 +315,12 @@ describe.skip('', () => {
test('should send the correct HTTP request to delete an index template', async () => {
const { component, actions, table } = testBed;
- const { rows } = table.getMetaData('templatesTable');
+ const { rows } = table.getMetaData('templateTable');
- const watchId = rows[0].columns[2].value;
+ const templateId = rows[0].columns[2].value;
- await actions.clickTemplateActionAt(0, 'delete');
+ const { name: templateName } = template1;
+ await actions.clickTemplateAction(templateName, 'delete');
const modal = document.body.querySelector(
'[data-test-subj="deleteTemplatesConfirmation"]'
@@ -290,7 +331,7 @@ describe.skip('', () => {
httpRequestsMockHelpers.setDeleteTemplateResponse({
results: {
- successes: [watchId],
+ successes: [templateId],
errors: [],
},
});
@@ -309,94 +350,77 @@ describe.skip('', () => {
});
});
- describe('detail flyout', () => {
- it('should have a close button and be able to close flyout', async () => {
+ describe('detail panel', () => {
+ beforeEach(async () => {
const template = fixtures.getTemplate({
name: `a${getRandomString()}`,
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
});
- const { actions, component, exists } = testBed;
-
httpRequestsMockHelpers.setLoadTemplateResponse(template);
+ });
- await actions.clickTemplateAt(0);
+ test('should show details when clicking on a template', async () => {
+ const { exists, actions } = testBed;
- // @ts-ignore (remove when react 16.9.0 is released)
- await act(async () => {
- await nextTick();
- component.update();
- });
+ expect(exists('templateDetails')).toBe(false);
- expect(exists('closeDetailsButton')).toBe(true);
- expect(exists('summaryTab')).toBe(true);
+ await actions.clickTemplateAt(0);
- actions.clickCloseDetailsButton();
+ expect(exists('templateDetails')).toBe(true);
+ });
- // @ts-ignore (remove when react 16.9.0 is released)
- await act(async () => {
- await nextTick();
- component.update();
+ describe('on mount', () => {
+ beforeEach(async () => {
+ const { actions } = testBed;
+
+ await actions.clickTemplateAt(0);
});
- expect(exists('summaryTab')).toBe(false);
- });
+ test('should set the correct title', async () => {
+ const { find } = testBed;
+ const { name } = template1;
- it('should have a delete button', async () => {
- const template = fixtures.getTemplate({
- name: `a${getRandomString()}`,
- indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
+ expect(find('templateDetails.title').text()).toEqual(name);
});
- const { actions, component, exists } = testBed;
-
- httpRequestsMockHelpers.setLoadTemplateResponse(template);
+ it('should have a close button and be able to close flyout', async () => {
+ const { actions, component, exists } = testBed;
- await actions.clickTemplateAt(0);
+ expect(exists('closeDetailsButton')).toBe(true);
+ expect(exists('summaryTab')).toBe(true);
- // @ts-ignore (remove when react 16.9.0 is released)
- await act(async () => {
- await nextTick();
- component.update();
- });
+ actions.clickCloseDetailsButton();
- expect(exists('templateDetails.deleteTemplateButton')).toBe(true);
- });
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ await nextTick();
+ component.update();
+ });
- it('should render an error if error fetching template details', async () => {
- const { actions, component, exists } = testBed;
- const error = {
- status: 404,
- error: 'Not found',
- message: 'Template not found',
- };
+ expect(exists('summaryTab')).toBe(false);
+ });
- httpRequestsMockHelpers.setLoadTemplateResponse(undefined, { body: error });
+ it('should have a manage button', async () => {
+ const { actions, exists } = testBed;
- await actions.clickTemplateAt(0);
+ await actions.clickTemplateAt(0);
- // @ts-ignore (remove when react 16.9.0 is released)
- await act(async () => {
- await nextTick();
- component.update();
+ expect(exists('templateDetails.manageTemplateButton')).toBe(true);
});
-
- expect(exists('sectionError')).toBe(true);
- // Delete button should not render if error
- expect(exists('templateDetails.deleteTemplateButton')).toBe(false);
});
describe('tabs', () => {
- test('should have 4 tabs if template has mappings, settings and aliases data', async () => {
+ test('should have 4 tabs', async () => {
const template = fixtures.getTemplate({
name: `a${getRandomString()}`,
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
- settings: {
+ settings: JSON.stringify({
index: {
number_of_shards: '1',
},
- },
- mappings: {
+ }),
+ mappings: JSON.stringify({
_source: {
enabled: false,
},
@@ -406,13 +430,13 @@ describe.skip('', () => {
format: 'EEE MMM dd HH:mm:ss Z yyyy',
},
},
- },
- aliases: {
+ }),
+ aliases: JSON.stringify({
alias1: {},
- },
+ }),
});
- const { find, actions, exists, component } = testBed;
+ const { find, actions, exists } = testBed;
httpRequestsMockHelpers.setLoadTemplateResponse(template);
@@ -431,51 +455,30 @@ describe.skip('', () => {
// Navigate and verify all tabs
actions.selectDetailsTab('settings');
-
- // @ts-ignore (remove when react 16.9.0 is released)
- await act(async () => {
- await nextTick();
- component.update();
- });
-
expect(exists('summaryTab')).toBe(false);
expect(exists('settingsTab')).toBe(true);
actions.selectDetailsTab('aliases');
-
- // @ts-ignore (remove when react 16.9.0 is released)
- await act(async () => {
- await nextTick();
- component.update();
- });
-
expect(exists('summaryTab')).toBe(false);
expect(exists('settingsTab')).toBe(false);
expect(exists('aliasesTab')).toBe(true);
actions.selectDetailsTab('mappings');
-
- // @ts-ignore (remove when react 16.9.0 is released)
- await act(async () => {
- await nextTick();
- component.update();
- });
-
expect(exists('summaryTab')).toBe(false);
expect(exists('settingsTab')).toBe(false);
expect(exists('aliasesTab')).toBe(false);
expect(exists('mappingsTab')).toBe(true);
});
- it('should not show tabs if mappings, settings and aliases data is not present', async () => {
- const templateWithNoTabs = fixtures.getTemplate({
+ test('should show an info callout if data is not present', async () => {
+ const templateWithNoOptionalFields = fixtures.getTemplate({
name: `a${getRandomString()}`,
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
});
const { actions, find, exists, component } = testBed;
- httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoTabs);
+ httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoOptionalFields);
await actions.clickTemplateAt(0);
@@ -485,41 +488,37 @@ describe.skip('', () => {
component.update();
});
- expect(find('templateDetails.tab').length).toBe(0);
+ expect(find('templateDetails.tab').length).toBe(4);
expect(exists('summaryTab')).toBe(true);
- expect(exists('summaryTitle')).toBe(true);
- });
- it('should not show all tabs if mappings, settings or aliases data is not present', async () => {
- const templateWithSomeTabs = fixtures.getTemplate({
- name: `a${getRandomString()}`,
- indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
- settings: {
- index: {
- number_of_shards: '1',
- },
- },
- });
+ // Navigate and verify callout message per tab
+ actions.selectDetailsTab('settings');
+ expect(exists('noSettingsCallout')).toBe(true);
- const { actions, find, exists, component } = testBed;
+ actions.selectDetailsTab('mappings');
+ expect(exists('noMappingsCallout')).toBe(true);
+
+ actions.selectDetailsTab('aliases');
+ expect(exists('noAliasesCallout')).toBe(true);
+ });
+ });
- httpRequestsMockHelpers.setLoadTemplateResponse(templateWithSomeTabs);
+ describe('error handling', () => {
+ it('should render an error message if error fetching template details', async () => {
+ const { actions, exists } = testBed;
+ const error = {
+ status: 404,
+ error: 'Not found',
+ message: 'Template not found',
+ };
- await actions.clickTemplateAt(0);
+ httpRequestsMockHelpers.setLoadTemplateResponse(undefined, { body: error });
- // @ts-ignore (remove when react 16.9.0 is released)
- await act(async () => {
- await nextTick();
- component.update();
- });
+ await actions.clickTemplateAt(0);
- expect(find('templateDetails.tab').length).toBe(2);
- expect(exists('summaryTab')).toBe(true);
- // Template does not contain aliases or mappings, so tabs will not render
- expect(find('templateDetails.tab').map(t => t.text())).toEqual([
- 'Summary',
- 'Settings',
- ]);
+ expect(exists('sectionError')).toBe(true);
+ // Manage button should not render if error
+ expect(exists('templateDetails.manageTemplateButton')).toBe(false);
});
});
});
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
new file mode 100644
index 0000000000000..7b333a5018cd2
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
@@ -0,0 +1,138 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import axiosXhrAdapter from 'axios/lib/adapters/xhr';
+import axios from 'axios';
+
+import { setupEnvironment, pageHelpers, nextTick } from './helpers';
+import { TemplateFormTestBed } from './helpers/template_form.helpers';
+import * as fixtures from '../../test/fixtures';
+import { TEMPLATE_NAME, INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS } from './helpers/constants';
+
+const { setup } = pageHelpers.templateClone;
+
+const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
+
+jest.mock('ui/index_patterns', () => ({
+ ILLEGAL_CHARACTERS: 'ILLEGAL_CHARACTERS',
+ CONTAINS_SPACES: 'CONTAINS_SPACES',
+ validateIndexPattern: () => {
+ return {
+ errors: {},
+ };
+ },
+}));
+
+jest.mock('ui/chrome', () => ({
+ breadcrumbs: { set: () => {} },
+ addBasePath: (path: string) => path || '/api/index_management',
+}));
+
+jest.mock('../../public/services/api', () => ({
+ ...jest.requireActual('../../public/services/api'),
+ getHttpClient: () => mockHttpClient,
+}));
+
+jest.mock('@elastic/eui', () => ({
+ ...jest.requireActual('@elastic/eui'),
+ // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
+ // which does not produce a valid component wrapper
+ EuiComboBox: (props: any) => (
+ {
+ props.onChange([syntheticEvent['0']]);
+ }}
+ />
+ ),
+ // Mocking EuiCodeEditor, which uses React Ace under the hood
+ EuiCodeEditor: (props: any) => (
+ {
+ props.onChange(syntheticEvent.jsonString);
+ }}
+ />
+ ),
+}));
+
+// We need to skip the tests until react 16.9.0 is released
+// which supports asynchronous code inside act()
+describe.skip('', () => {
+ let testBed: TemplateFormTestBed;
+
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ const templateToClone = fixtures.getTemplate({
+ name: TEMPLATE_NAME,
+ indexPatterns: ['indexPattern1'],
+ });
+
+ beforeEach(async () => {
+ testBed = await setup();
+
+ httpRequestsMockHelpers.setLoadTemplateResponse(templateToClone);
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ await nextTick();
+ testBed.component.update();
+ });
+ });
+
+ test('should set the correct page title', () => {
+ const { exists, find } = testBed;
+
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual(`Clone template '${templateToClone.name}'`);
+ });
+
+ describe('form payload', () => {
+ beforeEach(async () => {
+ const { actions } = testBed;
+
+ // Complete step 1 (logistics)
+ // Specify index patterns, but do not change name (keep default)
+ actions.completeStepOne({
+ indexPatterns: DEFAULT_INDEX_PATTERNS,
+ });
+
+ // Bypass step 2 (index settings)
+ actions.clickNextButton();
+
+ // Bypass step 3 (mappings)
+ actions.clickNextButton();
+
+ // Bypass step 4 (aliases)
+ actions.clickNextButton();
+ });
+
+ it('should send the correct payload', async () => {
+ const { actions } = testBed;
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ actions.clickSubmitButton();
+ await nextTick();
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ expect(latestRequest.requestBody).toEqual(
+ JSON.stringify({
+ ...templateToClone,
+ name: `${templateToClone.name}-copy`,
+ indexPatterns: DEFAULT_INDEX_PATTERNS,
+ })
+ );
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx
new file mode 100644
index 0000000000000..7fe699f4adfcf
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx
@@ -0,0 +1,355 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import axiosXhrAdapter from 'axios/lib/adapters/xhr';
+import axios from 'axios';
+
+import { setupEnvironment, pageHelpers, nextTick } from './helpers';
+import { TemplateFormTestBed } from './helpers/template_form.helpers';
+import {
+ TEMPLATE_NAME,
+ SETTINGS,
+ MAPPINGS,
+ ALIASES,
+ INDEX_PATTERNS as DEFAULT_INDEX_PATTERNS,
+} from './helpers/constants';
+
+const { setup } = pageHelpers.templateCreate;
+
+const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
+
+jest.mock('ui/index_patterns', () => ({
+ ILLEGAL_CHARACTERS: 'ILLEGAL_CHARACTERS',
+ CONTAINS_SPACES: 'CONTAINS_SPACES',
+ validateIndexPattern: () => {
+ return {
+ errors: {},
+ };
+ },
+}));
+
+jest.mock('ui/chrome', () => ({
+ breadcrumbs: { set: () => {} },
+ addBasePath: (path: string) => path || '/api/index_management',
+}));
+
+jest.mock('../../public/services/api', () => ({
+ ...jest.requireActual('../../public/services/api'),
+ getHttpClient: () => mockHttpClient,
+}));
+
+jest.mock('@elastic/eui', () => ({
+ ...jest.requireActual('@elastic/eui'),
+ // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
+ // which does not produce a valid component wrapper
+ EuiComboBox: (props: any) => (
+ {
+ props.onChange([syntheticEvent['0']]);
+ }}
+ />
+ ),
+ // Mocking EuiCodeEditor, which uses React Ace under the hood
+ EuiCodeEditor: (props: any) => (
+ {
+ props.onChange(syntheticEvent.jsonString);
+ }}
+ />
+ ),
+}));
+
+// We need to skip the tests until react 16.9.0 is released
+// which supports asynchronous code inside act()
+describe.skip('', () => {
+ let testBed: TemplateFormTestBed;
+
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ describe('on component mount', () => {
+ beforeEach(async () => {
+ testBed = await setup();
+ });
+
+ test('should set the correct page title', () => {
+ const { exists, find } = testBed;
+
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual('Create template');
+ });
+
+ test('should not let the user go to the next step with invalid fields', () => {
+ const { find, form } = testBed;
+
+ form.setInputValue('nameInput', '');
+ find('mockComboBox').simulate('change', [{ value: '' }]);
+
+ const nextButton = find('nextButton');
+ expect(nextButton.props().disabled).toEqual(true);
+ });
+
+ describe('form validation', () => {
+ beforeEach(async () => {
+ testBed = await setup();
+ });
+
+ describe('index settings (step 2)', () => {
+ beforeEach(() => {
+ const { actions } = testBed;
+
+ // Complete step 1 (logistics)
+ actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
+ });
+
+ it('should not allow invalid json', async () => {
+ const { form, actions, exists, find } = testBed;
+
+ // Complete step 2 (index settings) with invalid json
+ expect(exists('stepSettings')).toBe(true);
+ expect(find('stepTitle').text()).toEqual('Index settings (optional)');
+ actions.completeStepTwo({
+ settings: '{ invalidJsonString ',
+ });
+
+ expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
+ });
+ });
+
+ describe('mappings (step 3)', () => {
+ beforeEach(() => {
+ const { actions } = testBed;
+
+ // Complete step 1 (logistics)
+ actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
+
+ // Complete step 2 (index settings)
+ actions.completeStepTwo({
+ settings: '{}',
+ });
+ });
+
+ it('should not allow invalid json', async () => {
+ const { actions, form, exists, find } = testBed;
+
+ // Complete step 3 (mappings) with invalid json
+ expect(exists('stepMappings')).toBe(true);
+ expect(find('stepTitle').text()).toEqual('Mappings (optional)');
+ actions.completeStepThree({
+ mappings: '{ invalidJsonString ',
+ });
+
+ expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
+ });
+ });
+
+ describe('aliases (step 4)', () => {
+ beforeEach(() => {
+ const { actions } = testBed;
+
+ // Complete step 1 (logistics)
+ actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
+
+ // Complete step 2 (index settings)
+ actions.completeStepTwo({
+ settings: '{}',
+ });
+
+ // Complete step 3 (mappings)
+ actions.completeStepThree({
+ mappings: '{}',
+ });
+ });
+
+ it('should not allow invalid json', async () => {
+ const { actions, form, exists, find } = testBed;
+
+ // Complete step 4 (aliases) with invalid json
+ expect(exists('stepAliases')).toBe(true);
+ expect(find('stepTitle').text()).toEqual('Aliases (optional)');
+ actions.completeStepFour({
+ aliases: '{ invalidJsonString ',
+ });
+
+ expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
+ });
+ });
+ });
+
+ describe('review (step 5)', () => {
+ beforeEach(async () => {
+ testBed = await setup();
+
+ const { actions } = testBed;
+
+ // Complete step 1 (logistics)
+ actions.completeStepOne({
+ name: TEMPLATE_NAME,
+ indexPatterns: DEFAULT_INDEX_PATTERNS,
+ });
+
+ // Complete step 2 (index settings)
+ actions.completeStepTwo({
+ settings: JSON.stringify(SETTINGS),
+ });
+
+ // Complete step 3 (mappings)
+ actions.completeStepThree({
+ mappings: JSON.stringify(MAPPINGS),
+ });
+
+ // Complete step 4 (aliases)
+ actions.completeStepFour({
+ aliases: JSON.stringify(ALIASES),
+ });
+ });
+
+ it('should set the correct step title', () => {
+ const { find, exists } = testBed;
+ expect(exists('stepSummary')).toBe(true);
+ expect(find('stepTitle').text()).toEqual(`Review details for '${TEMPLATE_NAME}'`);
+ });
+
+ describe('tabs', () => {
+ test('should have 2 tabs', () => {
+ const { find } = testBed;
+
+ expect(find('summaryTabContent').find('.euiTab').length).toBe(2);
+ expect(
+ find('summaryTabContent')
+ .find('.euiTab')
+ .map(t => t.text())
+ ).toEqual(['Summary', 'Request']);
+ });
+
+ test('should navigate to the Request tab', async () => {
+ const { exists, actions } = testBed;
+
+ expect(exists('summaryTab')).toBe(true);
+ expect(exists('requestTab')).toBe(false);
+
+ actions.selectSummaryTab('request');
+
+ expect(exists('summaryTab')).toBe(false);
+ expect(exists('requestTab')).toBe(true);
+ });
+ });
+
+ it('should render a warning message if a wildcard is used as an index pattern', async () => {
+ testBed = await setup();
+
+ const { actions, exists, find } = testBed;
+
+ // Complete step 1 (logistics)
+ actions.completeStepOne({
+ name: TEMPLATE_NAME,
+ indexPatterns: ['*'], // Set wildcard index pattern
+ });
+
+ // Complete step 2 (index settings)
+ actions.completeStepTwo({
+ settings: JSON.stringify({}),
+ });
+
+ // Complete step 3 (mappings)
+ actions.completeStepThree({
+ mappings: JSON.stringify({}),
+ });
+
+ // Complete step 4 (aliases)
+ actions.completeStepFour({
+ aliases: JSON.stringify({}),
+ });
+
+ expect(exists('indexPatternsWarning')).toBe(true);
+ expect(find('indexPatternsWarningDescription').text()).toEqual(
+ 'All new indices that you create will use this template. Edit index patterns.'
+ );
+ });
+ });
+
+ describe('form payload & api errors', () => {
+ beforeEach(async () => {
+ testBed = await setup();
+
+ const { actions } = testBed;
+
+ // Complete step 1 (logistics)
+ actions.completeStepOne({
+ name: TEMPLATE_NAME,
+ indexPatterns: DEFAULT_INDEX_PATTERNS,
+ });
+
+ // Complete step 2 (index settings)
+ actions.completeStepTwo({
+ settings: JSON.stringify(SETTINGS),
+ });
+
+ // Complete step 3 (mappings)
+ actions.completeStepThree({
+ mappings: JSON.stringify(MAPPINGS),
+ });
+
+ // Complete step 4 (aliases)
+ actions.completeStepFour({
+ aliases: JSON.stringify(ALIASES),
+ });
+ });
+
+ it('should send the correct payload', async () => {
+ const { actions } = testBed;
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ actions.clickSubmitButton();
+ await nextTick();
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ expect(latestRequest.requestBody).toEqual(
+ JSON.stringify({
+ name: TEMPLATE_NAME,
+ indexPatterns: DEFAULT_INDEX_PATTERNS,
+ version: '',
+ order: '',
+ settings: JSON.stringify(SETTINGS),
+ mappings: JSON.stringify(MAPPINGS),
+ aliases: JSON.stringify(ALIASES),
+ })
+ );
+ });
+
+ it('should surface the API errors from the put HTTP request', async () => {
+ const { component, actions, find, exists } = testBed;
+
+ const error = {
+ status: 409,
+ error: 'Conflict',
+ message: `There is already a template with name '${TEMPLATE_NAME}'`,
+ };
+
+ httpRequestsMockHelpers.setCreateTemplateResponse(undefined, { body: error });
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ actions.clickSubmitButton();
+ await nextTick();
+ component.update();
+ });
+
+ expect(exists('saveTemplateError')).toBe(true);
+ expect(find('saveTemplateError').text()).toContain(error.message);
+ });
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
new file mode 100644
index 0000000000000..9db54e32d15f3
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
@@ -0,0 +1,155 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import axiosXhrAdapter from 'axios/lib/adapters/xhr';
+import axios from 'axios';
+
+import { setupEnvironment, pageHelpers, nextTick } from './helpers';
+import { TemplateFormTestBed } from './helpers/template_form.helpers';
+import * as fixtures from '../../test/fixtures';
+import { TEMPLATE_NAME, SETTINGS, MAPPINGS, ALIASES } from './helpers/constants';
+
+const UPDATED_INDEX_PATTERN = ['updatedIndexPattern'];
+
+const { setup } = pageHelpers.templateEdit;
+
+const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
+
+jest.mock('ui/index_patterns', () => ({
+ ILLEGAL_CHARACTERS: 'ILLEGAL_CHARACTERS',
+ CONTAINS_SPACES: 'CONTAINS_SPACES',
+ validateIndexPattern: () => {
+ return {
+ errors: {},
+ };
+ },
+}));
+
+jest.mock('ui/chrome', () => ({
+ breadcrumbs: { set: () => {} },
+ addBasePath: (path: string) => path || '/api/index_management',
+}));
+
+jest.mock('../../public/services/api', () => ({
+ ...jest.requireActual('../../public/services/api'),
+ getHttpClient: () => mockHttpClient,
+}));
+
+jest.mock('@elastic/eui', () => ({
+ ...jest.requireActual('@elastic/eui'),
+ // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
+ // which does not produce a valid component wrapper
+ EuiComboBox: (props: any) => (
+ {
+ props.onChange([syntheticEvent['0']]);
+ }}
+ />
+ ),
+ // Mocking EuiCodeEditor, which uses React Ace under the hood
+ EuiCodeEditor: (props: any) => (
+ {
+ props.onChange(syntheticEvent.jsonString);
+ }}
+ />
+ ),
+}));
+
+// We need to skip the tests until react 16.9.0 is released
+// which supports asynchronous code inside act()
+describe.skip('', () => {
+ let testBed: TemplateFormTestBed;
+
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ const templateToEdit = fixtures.getTemplate({
+ name: TEMPLATE_NAME,
+ indexPatterns: ['indexPattern1'],
+ });
+
+ beforeEach(async () => {
+ testBed = await setup();
+
+ httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit);
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ await nextTick();
+ testBed.component.update();
+ });
+ });
+
+ test('should set the correct page title', () => {
+ const { exists, find } = testBed;
+ const { name } = templateToEdit;
+
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual(`Edit template '${name}'`);
+ });
+
+ describe('form payload', () => {
+ beforeEach(async () => {
+ const { actions, find } = testBed;
+
+ // Step 1 (logistics)
+ const nameInput = find('nameInput');
+ expect(nameInput.props().readOnly).toEqual(true);
+
+ actions.completeStepOne({
+ indexPatterns: UPDATED_INDEX_PATTERN,
+ });
+
+ // Step 2 (index settings)
+ actions.completeStepTwo({
+ settings: JSON.stringify(SETTINGS),
+ });
+
+ // Step 3 (mappings)
+ actions.completeStepThree({
+ mappings: JSON.stringify(MAPPINGS),
+ });
+
+ // Step 4 (aliases)
+ actions.completeStepFour({
+ aliases: JSON.stringify(ALIASES),
+ });
+ });
+
+ it('should send the correct payload with changed values', async () => {
+ const { actions } = testBed;
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ actions.clickSubmitButton();
+ await nextTick();
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ const { version, order } = templateToEdit;
+
+ expect(latestRequest.requestBody).toEqual(
+ JSON.stringify({
+ name: TEMPLATE_NAME,
+ version,
+ order,
+ indexPatterns: UPDATED_INDEX_PATTERN,
+ settings: JSON.stringify(SETTINGS),
+ mappings: JSON.stringify(MAPPINGS),
+ aliases: JSON.stringify(ALIASES),
+ })
+ );
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/legacy/plugins/index_management/__jest__/components/index_table.test.js
index fb3c6f1443ce4..d6a53eefcdc95 100644
--- a/x-pack/legacy/plugins/index_management/__jest__/components/index_table.test.js
+++ b/x-pack/legacy/plugins/index_management/__jest__/components/index_table.test.js
@@ -19,23 +19,17 @@ import { setHttpClient } from '../../public/services/api';
import sinon from 'sinon';
import { findTestSubject } from '@elastic/eui/lib/test';
-jest.mock('react-ace', () => {
- const { PureComponent } = require('react');
- return class extends PureComponent {
- editor = {
- textInput: {
- getElement() {
- return { removeAttribute() {}, addEventListener() {} };
- }
- }
- };
- render() {
- return ;
- }
- };
-});
-jest.mock('brace/theme/textmate', () => 'brace/theme/textmate');
-jest.mock('brace/ext/language_tools', () => 'brace/ext/language_tools');
+
+jest.mock('ui/chrome', () => ({
+ breadcrumbs: { set: () => { } },
+ addBasePath: path => path || '/api/index_management',
+}));
+
+jest.mock('ui/index_patterns', () => ({
+ ILLEGAL_CHARACTERS: '',
+ CONTAINS_SPACES: '',
+ validateIndexPattern: () => { },
+}));
setHttpClient(axios.create({ adapter: axiosXhrAdapter }));
let server = null;
diff --git a/x-pack/legacy/plugins/index_management/common/constants/index.ts b/x-pack/legacy/plugins/index_management/common/constants/index.ts
index 06159fd45ede1..de8a548ecab53 100644
--- a/x-pack/legacy/plugins/index_management/common/constants/index.ts
+++ b/x-pack/legacy/plugins/index_management/common/constants/index.ts
@@ -6,6 +6,7 @@
export { PLUGIN } from './plugin';
export { BASE_PATH } from './base_path';
+export { INVALID_INDEX_PATTERN_CHARS } from './invalid_characters';
export * from './index_statuses';
export {
@@ -45,4 +46,7 @@ export {
UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
+ UIM_TEMPLATE_CREATE,
+ UIM_TEMPLATE_UPDATE,
+ UIM_TEMPLATE_CLONE,
} from './ui_metric';
diff --git a/x-pack/legacy/plugins/index_management/common/constants/invalid_characters.ts b/x-pack/legacy/plugins/index_management/common/constants/invalid_characters.ts
new file mode 100644
index 0000000000000..26b1df2a7ab87
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/common/constants/invalid_characters.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const INVALID_INDEX_PATTERN_CHARS = ['\\', '/', '?', '"', '<', '>', '|'];
diff --git a/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts b/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts
index 7f0c62ddf5ec0..5fda812c704d1 100644
--- a/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts
+++ b/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts
@@ -41,3 +41,6 @@ export const UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB = 'template_details_summary_t
export const UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB = 'template_details_settings_tab';
export const UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB = 'template_details_mappings_tab';
export const UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB = 'template_details_aliases_tab';
+export const UIM_TEMPLATE_CREATE = 'template_create';
+export const UIM_TEMPLATE_UPDATE = 'template_update';
+export const UIM_TEMPLATE_CLONE = 'template_clone';
diff --git a/x-pack/legacy/plugins/index_management/common/lib/index.ts b/x-pack/legacy/plugins/index_management/common/lib/index.ts
new file mode 100644
index 0000000000000..83b22d8d72e92
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/common/lib/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+export {
+ deserializeTemplateList,
+ deserializeTemplate,
+ serializeTemplate,
+} from './template_serialization';
diff --git a/x-pack/legacy/plugins/index_management/common/lib/template_serialization.ts b/x-pack/legacy/plugins/index_management/common/lib/template_serialization.ts
new file mode 100644
index 0000000000000..15e8a8b77e164
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/common/lib/template_serialization.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Template, TemplateEs, TemplateListItem } from '../types';
+
+const parseJson = (jsonString: string) => {
+ let parsedJson;
+
+ try {
+ parsedJson = JSON.parse(jsonString);
+
+ // Do not send empty object
+ if (!hasEntries(parsedJson)) {
+ parsedJson = undefined;
+ }
+ } catch (e) {
+ // Silently swallow parsing errors since parsing validation is done on client
+ // so we should never reach this point
+ }
+
+ return parsedJson;
+};
+
+const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
+
+const stringifyJson = (json: any) => {
+ return JSON.stringify(json, null, 2);
+};
+
+export function deserializeTemplateList(indexTemplatesByName: any): TemplateListItem[] {
+ const indexTemplateNames: string[] = Object.keys(indexTemplatesByName);
+
+ const deserializedTemplates: TemplateListItem[] = indexTemplateNames.map((name: string) => {
+ const {
+ version,
+ order,
+ index_patterns: indexPatterns = [],
+ settings = {},
+ aliases = {},
+ mappings = {},
+ } = indexTemplatesByName[name];
+
+ return {
+ name,
+ version,
+ order,
+ indexPatterns: indexPatterns.sort(),
+ hasSettings: hasEntries(settings),
+ hasAliases: hasEntries(aliases),
+ hasMappings: hasEntries(mappings),
+ ilmPolicy: settings && settings.index && settings.index.lifecycle,
+ };
+ });
+
+ return deserializedTemplates;
+}
+
+export function serializeTemplate(template: Template): TemplateEs {
+ const { name, version, order, indexPatterns, settings, aliases, mappings } = template;
+
+ const serializedTemplate: TemplateEs = {
+ name,
+ version: version ? Number(version) : undefined,
+ order: order ? Number(order) : undefined,
+ index_patterns: indexPatterns,
+ settings: settings ? parseJson(settings) : undefined,
+ aliases: aliases ? parseJson(aliases) : undefined,
+ mappings: mappings ? parseJson(mappings) : undefined,
+ };
+
+ return serializedTemplate;
+}
+
+export function deserializeTemplate(templateEs: TemplateEs): Template {
+ const {
+ name,
+ version,
+ order,
+ index_patterns: indexPatterns,
+ settings,
+ aliases,
+ mappings,
+ } = templateEs;
+
+ const deserializedTemplate: Template = {
+ name,
+ version: version || version === 0 ? version : '',
+ order: order || order === 0 ? order : '',
+ indexPatterns: indexPatterns.sort(),
+ settings: hasEntries(settings) ? stringifyJson(settings) : undefined,
+ aliases: hasEntries(aliases) ? stringifyJson(aliases) : undefined,
+ mappings: hasEntries(mappings) ? stringifyJson(mappings) : undefined,
+ ilmPolicy: settings && settings.index && settings.index.lifecycle,
+ };
+
+ return deserializedTemplate;
+}
diff --git a/x-pack/legacy/plugins/index_management/common/types/templates.ts b/x-pack/legacy/plugins/index_management/common/types/templates.ts
index b726451d6ac00..7d9a3e0588a4d 100644
--- a/x-pack/legacy/plugins/index_management/common/types/templates.ts
+++ b/x-pack/legacy/plugins/index_management/common/types/templates.ts
@@ -4,12 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export interface TemplateListItem {
+ name: string;
+ indexPatterns: string[];
+ version?: number;
+ order?: number;
+ hasSettings: boolean;
+ hasAliases: boolean;
+ hasMappings: boolean;
+ ilmPolicy?: {
+ name: string;
+ };
+}
export interface Template {
name: string;
- version: number;
- order: number;
indexPatterns: string[];
+ version?: number | '';
+ order?: number | '';
+ settings?: string;
+ aliases?: string;
+ mappings?: string;
+ ilmPolicy?: {
+ name: string;
+ };
+}
+
+export interface TemplateEs {
+ name: string;
+ index_patterns: string[];
+ version?: number;
+ order?: number;
settings?: {
+ [key: string]: any;
index?: {
[key: string]: any;
lifecycle?: {
@@ -17,6 +43,8 @@ export interface Template {
};
};
};
- aliases?: object;
+ aliases?: {
+ [key: string]: any;
+ };
mappings?: object;
}
diff --git a/x-pack/legacy/plugins/index_management/index.js b/x-pack/legacy/plugins/index_management/index.js
index e68fe4fb6325f..5e251ff3360df 100644
--- a/x-pack/legacy/plugins/index_management/index.js
+++ b/x-pack/legacy/plugins/index_management/index.js
@@ -7,7 +7,7 @@
import { resolve } from 'path';
import { createRouter } from '../../server/lib/create_router';
import { registerIndicesRoutes } from './server/routes/api/indices';
-import { registerTemplatesRoutes } from './server/routes/api/templates';
+import { registerTemplateRoutes } from './server/routes/api/templates';
import { registerMappingRoute } from './server/routes/api/mapping';
import { registerSettingsRoutes } from './server/routes/api/settings';
import { registerStatsRoute } from './server/routes/api/stats';
@@ -32,7 +32,7 @@ export function indexManagement(kibana) {
server.expose('addIndexManagementDataEnricher', addIndexManagementDataEnricher);
registerLicenseChecker(server, PLUGIN.ID, PLUGIN.NAME, PLUGIN.MINIMUM_LICENSE_REQUIRED);
registerIndicesRoutes(router);
- registerTemplatesRoutes(router);
+ registerTemplateRoutes(router);
registerSettingsRoutes(router);
registerStatsRoute(router);
registerMappingRoute(router);
diff --git a/x-pack/legacy/plugins/index_management/public/app.js b/x-pack/legacy/plugins/index_management/public/app.js
index 8f25183a21bc1..07fa15a1a1332 100644
--- a/x-pack/legacy/plugins/index_management/public/app.js
+++ b/x-pack/legacy/plugins/index_management/public/app.js
@@ -8,6 +8,9 @@ import React, { useEffect } from 'react';
import { HashRouter, Switch, Route, Redirect } from 'react-router-dom';
import { BASE_PATH, UIM_APP_LOAD } from '../common/constants';
import { IndexManagementHome } from './sections/home';
+import { TemplateCreate } from './sections/template_create';
+import { TemplateClone } from './sections/template_clone';
+import { TemplateEdit } from './sections/template_edit';
import { trackUiMetric } from './services';
export const App = () => {
@@ -20,9 +23,16 @@ export const App = () => {
);
};
-// Exoprt this so we can test it with a different router.
+// Export this so we can test it with a different router.
export const AppWithoutRouter = () => (
+
+
+
diff --git a/x-pack/legacy/plugins/index_management/public/components/index.ts b/x-pack/legacy/plugins/index_management/public/components/index.ts
index ee53e0f069203..be64f5756c56c 100644
--- a/x-pack/legacy/plugins/index_management/public/components/index.ts
+++ b/x-pack/legacy/plugins/index_management/public/components/index.ts
@@ -8,4 +8,5 @@ export { SectionError } from './section_error';
export { SectionLoading } from './section_loading';
export { NoMatch } from './no_match';
export { PageErrorForbidden } from './page_error';
-export { DeleteTemplatesModal } from './delete_templates_modal';
+export { TemplateDeleteModal } from './template_delete_modal';
+export { TemplateForm } from './template_form';
diff --git a/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx b/x-pack/legacy/plugins/index_management/public/components/template_delete_modal.tsx
similarity index 81%
rename from x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx
rename to x-pack/legacy/plugins/index_management/public/components/template_delete_modal.tsx
index 926be62739265..7cfa4335fa2fb 100644
--- a/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx
+++ b/x-pack/legacy/plugins/index_management/public/components/template_delete_modal.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox } from '@elastic/eui';
+import { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox, EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Fragment, useState } from 'react';
@@ -12,13 +12,15 @@ import { toastNotifications } from 'ui/notify';
import { deleteTemplates } from '../services/api';
import { Template } from '../../common/types';
-export const DeleteTemplatesModal = ({
+export const TemplateDeleteModal = ({
templatesToDelete,
callback,
}: {
templatesToDelete: Array;
callback: (data?: { hasDeletedTemplates: boolean }) => void;
}) => {
+ const [isDeleteConfirmed, setIsDeleteConfirmed] = useState(false);
+
const numTemplatesToDelete = templatesToDelete.length;
const hasSystemTemplate = Boolean(
@@ -30,13 +32,23 @@ export const DeleteTemplatesModal = ({
const hasDeletedTemplates = templatesDeleted && templatesDeleted.length;
if (hasDeletedTemplates) {
- const successMessage = i18n.translate(
- 'xpack.idxMgmt.deleteTemplatesModal.successNotificationMessageText',
- {
- defaultMessage: 'Deleted {numSuccesses, plural, one {# template} other {# templates}}',
- values: { numSuccesses: templatesDeleted.length },
- }
- );
+ const successMessage =
+ templatesDeleted.length === 1
+ ? i18n.translate(
+ 'xpack.idxMgmt.deleteTemplatesModal.successDeleteSingleNotificationMessageText',
+ {
+ defaultMessage: "Deleted template '{templateName}'",
+ values: { templateName: templatesToDelete[0] },
+ }
+ )
+ : i18n.translate(
+ 'xpack.idxMgmt.deleteTemplatesModal.successDeleteMultipleNotificationMessageText',
+ {
+ defaultMessage:
+ 'Deleted {numSuccesses, plural, one {# template} other {# templates}}',
+ values: { numSuccesses: templatesDeleted.length },
+ }
+ );
callback({ hasDeletedTemplates });
toastNotifications.addSuccess(successMessage);
@@ -69,8 +81,6 @@ export const DeleteTemplatesModal = ({
callback();
};
- const [isDeleteConfirmed, setIsDeleteConfirmed] = useState(false);
-
return (
{' '}
-
+
+
+
) : null}
@@ -141,7 +153,7 @@ export const DeleteTemplatesModal = ({
= ({
+ template,
+ updateTemplate,
+ errors,
+}) => {
+ const { aliases } = template;
+ const { aliases: aliasesError } = errors;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Aliases code editor */}
+
+ }
+ helpText={
+
+ {JSON.stringify({
+ my_alias: {},
+ })}
+
+ ),
+ }}
+ />
+ }
+ isInvalid={Boolean(aliasesError)}
+ error={aliasesError}
+ fullWidth
+ >
+ {
+ updateTemplate({ aliases: newAliases });
+ }}
+ data-test-subj="aliasesEditor"
+ />
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_logistics.tsx b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_logistics.tsx
new file mode 100644
index 0000000000000..e64be248b8571
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_logistics.tsx
@@ -0,0 +1,274 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useState } from 'react';
+import {
+ EuiComboBox,
+ EuiComboBoxOptionProps,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiButtonEmpty,
+ EuiSpacer,
+ EuiDescribedFormGroup,
+ EuiFormRow,
+ EuiFieldText,
+ EuiFieldNumber,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { Template } from '../../../../common/types';
+import { INVALID_INDEX_PATTERN_CHARS } from '../../../../common/constants';
+import { templatesDocumentationLink } from '../../../lib/documentation_links';
+import { StepProps } from '../types';
+
+const indexPatternInvalidCharacters = INVALID_INDEX_PATTERN_CHARS.join(' ');
+
+export const StepLogistics: React.FunctionComponent = ({
+ template,
+ updateTemplate,
+ errors,
+ isEditing,
+}) => {
+ const { name, order, version, indexPatterns } = template;
+ const { name: nameError, indexPatterns: indexPatternsError } = errors;
+
+ // hooks
+ const [allIndexPatterns, setAllIndexPatterns] = useState([]);
+ const [touchedFields, setTouchedFields] = useState({
+ name: false,
+ indexPatterns: false,
+ });
+
+ const indexPatternOptions = indexPatterns
+ ? indexPatterns.map(pattern => ({ label: pattern, value: pattern }))
+ : [];
+
+ const { name: isNameTouched, indexPatterns: isIndexPatternsTouched } = touchedFields;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Name */}
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="stepLogisticsNameDescription"
+ fullWidth
+ >
+
+ }
+ isInvalid={isNameTouched && Boolean(nameError)}
+ error={nameError}
+ fullWidth
+ >
+ setTouchedFields(prevTouched => ({ ...prevTouched, name: true }))}
+ data-test-subj="nameInput"
+ onChange={e => {
+ updateTemplate({ name: e.target.value });
+ }}
+ fullWidth
+ />
+
+
+ {/* Index patterns */}
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="stepLogisticsIndexPatternsDescription"
+ fullWidth
+ >
+
+ }
+ helpText={
+ {indexPatternInvalidCharacters},
+ }}
+ />
+ }
+ isInvalid={isIndexPatternsTouched && Boolean(indexPatternsError)}
+ error={indexPatternsError}
+ fullWidth
+ >
+
+ setTouchedFields(prevTouched => ({ ...prevTouched, indexPatterns: true }))
+ }
+ onChange={(selectedPattern: EuiComboBoxOptionProps[]) => {
+ const newIndexPatterns = selectedPattern.map(({ value }) => value as string);
+ updateTemplate({ indexPatterns: newIndexPatterns });
+ }}
+ onCreateOption={(selectedPattern: string) => {
+ if (!selectedPattern.trim().length) {
+ return;
+ }
+
+ const newIndexPatterns = [...indexPatterns, selectedPattern];
+
+ setAllIndexPatterns([...allIndexPatterns, selectedPattern]);
+ updateTemplate({ indexPatterns: newIndexPatterns });
+ }}
+ />
+
+
+ {/* Order */}
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="stepLogisticsOrderDescription"
+ fullWidth
+ >
+
+ }
+ >
+ {
+ const value = e.target.value;
+ updateTemplate({ order: value === '' ? value : Number(value) });
+ }}
+ data-test-subj="orderInput"
+ />
+
+ {' '}
+ {/* Version */}
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="stepLogisticsVersionDescription"
+ fullWidth
+ >
+
+ }
+ >
+ {
+ const value = e.target.value;
+ updateTemplate({ version: value === '' ? value : Number(value) });
+ }}
+ data-test-subj="versionInput"
+ />
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx
new file mode 100644
index 0000000000000..272bfa73d29e8
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx
@@ -0,0 +1,133 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiButtonEmpty,
+ EuiSpacer,
+ EuiFormRow,
+ EuiText,
+ EuiCodeEditor,
+ EuiCode,
+} from '@elastic/eui';
+import { mappingDocumentationLink } from '../../../lib/documentation_links';
+import { StepProps } from '../types';
+
+export const StepMappings: React.FunctionComponent = ({
+ template,
+ updateTemplate,
+ errors,
+}) => {
+ const { mappings } = template;
+ const { mappings: mappingsError } = errors;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Mappings code editor */}
+
+ }
+ helpText={
+
+ {JSON.stringify({
+ properties: {
+ name: { type: 'text' },
+ },
+ })}
+
+ ),
+ }}
+ />
+ }
+ isInvalid={Boolean(mappingsError)}
+ error={mappingsError}
+ fullWidth
+ >
+ {
+ updateTemplate({ mappings: newMappings });
+ }}
+ data-test-subj="mappingsEditor"
+ />
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_review.tsx b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_review.tsx
new file mode 100644
index 0000000000000..f9ba77af24d9e
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_review.tsx
@@ -0,0 +1,249 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFlexGroup,
+ EuiTitle,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiTabbedContent,
+ EuiDescriptionList,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
+ EuiText,
+ EuiCallOut,
+ EuiLink,
+ EuiCodeBlock,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { serializeTemplate } from '../../../../common/lib/template_serialization';
+import { StepProps } from '../types';
+
+const NoneDescriptionText = () => (
+
+);
+
+const getDescriptionText = (data: any) => {
+ const hasEntries = data && Object.entries(data).length > 0;
+
+ return hasEntries ? (
+
+ ) : (
+
+ );
+};
+
+export const StepReview: React.FunctionComponent = ({ template, updateCurrentStep }) => {
+ const { name, indexPatterns, version, order } = template;
+
+ const serializedTemplate = serializeTemplate(template);
+ // Name not included in ES request body
+ delete serializedTemplate.name;
+ const {
+ mappings: serializedMappings,
+ settings: serializedSettings,
+ aliases: serializedAliases,
+ } = serializedTemplate;
+
+ const numIndexPatterns = indexPatterns.length;
+
+ const hasWildCardIndexPattern = Boolean(indexPatterns.find(pattern => pattern === '*'));
+
+ const SummaryTab = () => (
+
+
+
+
+
+
+
+
+
+
+ {numIndexPatterns > 1 ? (
+
+
+ {indexPatterns.map((indexName: string, i: number) => {
+ return (
+ -
+
+ {indexName}
+
+
+ );
+ })}
+
+
+ ) : (
+ indexPatterns.toString()
+ )}
+
+
+
+
+
+
+ {order ? order : }
+
+
+
+
+
+
+ {version ? version : }
+
+
+
+
+
+
+
+
+
+
+ {getDescriptionText(serializedSettings)}
+
+
+
+
+
+ {getDescriptionText(serializedMappings)}
+
+
+
+
+
+ {getDescriptionText(serializedAliases)}
+
+
+
+
+
+ );
+
+ const RequestTab = () => {
+ const endpoint = `PUT _template/${name || ''}`;
+ const templateString = JSON.stringify(serializedTemplate, null, 2);
+ const request = `${endpoint}\n${templateString}`;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {request}
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {hasWildCardIndexPattern ? (
+
+
+ }
+ color="warning"
+ iconType="help"
+ data-test-subj="indexPatternsWarning"
+ >
+
+ {' '}
+ {/* Edit link navigates back to step 1 (logistics) */}
+
+
+
+
+
+
+
+ ) : null}
+
+
,
+ },
+ {
+ id: 'request',
+ name: i18n.translate('xpack.idxMgmt.templateForm.stepReview.requestTabTitle', {
+ defaultMessage: 'Request',
+ }),
+ content:
,
+ },
+ ]}
+ />
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_settings.tsx b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_settings.tsx
new file mode 100644
index 0000000000000..30de591849277
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_settings.tsx
@@ -0,0 +1,125 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiButtonEmpty,
+ EuiSpacer,
+ EuiFormRow,
+ EuiText,
+ EuiCodeEditor,
+ EuiCode,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { settingsDocumentationLink } from '../../../lib/documentation_links';
+import { StepProps } from '../types';
+
+export const StepSettings: React.FunctionComponent = ({
+ template,
+ updateTemplate,
+ errors,
+}) => {
+ const { settings } = template;
+ const { settings: settingsError } = errors;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Settings code editor */}
+
+ }
+ helpText={
+
{JSON.stringify({ number_of_replicas: 1 })},
+ }}
+ />
+ }
+ isInvalid={Boolean(settingsError)}
+ error={settingsError}
+ fullWidth
+ >
+ {
+ updateTemplate({ settings: newSettings });
+ }}
+ data-test-subj="settingsEditor"
+ />
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/template_form.tsx b/x-pack/legacy/plugins/index_management/public/components/template_form/template_form.tsx
new file mode 100644
index 0000000000000..cf100df6436bc
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/template_form/template_form.tsx
@@ -0,0 +1,244 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { Fragment, useState, useEffect } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiForm,
+ EuiSpacer,
+} from '@elastic/eui';
+import { Template } from '../../../common/types';
+import { TemplateSteps } from './template_steps';
+import { StepAliases, StepLogistics, StepMappings, StepSettings, StepReview } from './steps';
+import {
+ validateLogistics,
+ validateSettings,
+ validateMappings,
+ validateAliases,
+ TemplateValidation,
+} from '../../services/template_validation';
+import { StepProps } from './types';
+import { SectionError } from '..';
+
+interface Props {
+ onSave: (template: Template) => void;
+ clearSaveError: () => void;
+ isSaving: boolean;
+ saveError: any;
+ template: Template;
+ isEditing?: boolean;
+}
+
+const defaultValidation = {
+ isValid: true,
+ errors: {},
+};
+
+const stepComponentMap: { [key: number]: React.FunctionComponent } = {
+ 1: StepLogistics,
+ 2: StepSettings,
+ 3: StepMappings,
+ 4: StepAliases,
+ 5: StepReview,
+};
+
+const stepValidationMap: { [key: number]: any } = {
+ 1: validateLogistics,
+ 2: validateSettings,
+ 3: validateMappings,
+ 4: validateAliases,
+};
+
+export const TemplateForm: React.FunctionComponent = ({
+ template: initialTemplate,
+ onSave,
+ isSaving,
+ saveError,
+ clearSaveError,
+ isEditing,
+}) => {
+ // hooks
+ const [currentStep, setCurrentStep] = useState(1);
+ const [template, setTemplate] = useState(initialTemplate);
+ const [validation, setValidation] = useState<{ [key: number]: TemplateValidation }>({
+ 1: defaultValidation,
+ 2: defaultValidation,
+ 3: defaultValidation,
+ 4: defaultValidation,
+ 5: defaultValidation,
+ });
+
+ const lastStep = Object.keys(stepComponentMap).length;
+
+ const CurrentStepComponent = stepComponentMap[currentStep];
+
+ const validateStep = stepValidationMap[currentStep];
+
+ const stepErrors = validation[currentStep].errors;
+ const isStepValid = validation[currentStep].isValid;
+
+ const updateValidation = (templateToValidate: Template): void => {
+ const stepValidation = validateStep(templateToValidate);
+
+ const newValidation = {
+ ...validation,
+ ...{
+ [currentStep]: stepValidation,
+ },
+ };
+
+ setValidation(newValidation);
+ };
+
+ const updateTemplate = (updatedTemplate: Partial): void => {
+ const newTemplate = { ...template, ...updatedTemplate };
+
+ updateValidation(newTemplate);
+ setTemplate(newTemplate);
+ };
+
+ const updateCurrentStep = (nextStep: number) => {
+ // All steps needs validation, except for the last step
+ const shouldValidate = currentStep !== lastStep;
+
+ // If step is invalid do not let user proceed
+ if (shouldValidate && !isStepValid) {
+ return;
+ }
+
+ setCurrentStep(nextStep);
+ clearSaveError();
+ };
+
+ const onBack = () => {
+ const prevStep = currentStep - 1;
+ updateCurrentStep(prevStep);
+ };
+
+ const onNext = () => {
+ const nextStep = currentStep + 1;
+ updateCurrentStep(nextStep);
+ };
+
+ const saveButtonLabel = isEditing ? (
+
+ ) : (
+
+ );
+
+ useEffect(() => {
+ if (!isEditing) {
+ updateValidation(template);
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+ {saveError ? (
+
+
+ }
+ error={saveError}
+ data-test-subj="saveTemplateError"
+ />
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+ {currentStep > 1 ? (
+
+
+
+
+
+ ) : null}
+
+ {currentStep < lastStep ? (
+
+
+
+
+
+ ) : null}
+
+ {currentStep === lastStep ? (
+
+
+ {isSaving ? (
+
+ ) : (
+ saveButtonLabel
+ )}
+
+
+ ) : null}
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/template_steps.tsx b/x-pack/legacy/plugins/index_management/public/components/template_form/template_steps.tsx
new file mode 100644
index 0000000000000..5603bb4173773
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/template_form/template_steps.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiStepsHorizontal } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+interface Props {
+ currentStep: number;
+ updateCurrentStep: (step: number, maxCompletedStep: number) => void;
+ isCurrentStepValid: boolean;
+}
+
+const stepNamesMap: { [key: number]: string } = {
+ 1: i18n.translate('xpack.idxMgmt.templateForm.steps.logisticsStepName', {
+ defaultMessage: 'Logistics',
+ }),
+ 2: i18n.translate('xpack.idxMgmt.templateForm.steps.settingsStepName', {
+ defaultMessage: 'Index settings',
+ }),
+ 3: i18n.translate('xpack.idxMgmt.templateForm.steps.mappingsStepName', {
+ defaultMessage: 'Mappings',
+ }),
+ 4: i18n.translate('xpack.idxMgmt.templateForm.steps.aliasesStepName', {
+ defaultMessage: 'Aliases',
+ }),
+ 5: i18n.translate('xpack.idxMgmt.templateForm.steps.summaryStepName', {
+ defaultMessage: 'Review template',
+ }),
+};
+
+export const TemplateSteps: React.FunctionComponent = ({
+ currentStep,
+ updateCurrentStep,
+ isCurrentStepValid,
+}) => {
+ const steps = [1, 2, 3, 4, 5].map(step => {
+ return {
+ title: stepNamesMap[step],
+ isComplete: currentStep > step,
+ isSelected: currentStep === step,
+ disabled: step !== currentStep && !isCurrentStepValid,
+ onClick: () => updateCurrentStep(step, step - 1),
+ };
+ });
+
+ return ;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/types.ts b/x-pack/legacy/plugins/index_management/public/components/template_form/types.ts
new file mode 100644
index 0000000000000..c3a474b183186
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/components/template_form/types.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Template } from '../../../common/types';
+import { TemplateValidation } from '../../services/template_validation';
+
+export interface StepProps {
+ template: Template;
+ updateTemplate: (updatedTemplate: Partial) => void;
+ updateCurrentStep: (step: number) => void;
+ errors: TemplateValidation['errors'];
+ isEditing?: boolean;
+}
diff --git a/x-pack/legacy/plugins/index_management/public/lib/documentation_links.ts b/x-pack/legacy/plugins/index_management/public/lib/documentation_links.ts
index d19787f5de3d0..4d6d5aeda527e 100644
--- a/x-pack/legacy/plugins/index_management/public/lib/documentation_links.ts
+++ b/x-pack/legacy/plugins/index_management/public/lib/documentation_links.ts
@@ -11,5 +11,7 @@ const esBase = `${base}/elasticsearch/reference/${DOC_LINK_VERSION}`;
const kibanaBase = `${base}/kibana/${DOC_LINK_VERSION}`;
export const settingsDocumentationLink = `${esBase}/index-modules.html#index-modules-settings`;
+export const mappingDocumentationLink = `${esBase}/mapping.html`;
+export const templatesDocumentationLink = `${esBase}/indices-templates.html`;
export const idxMgmtDocumentationLink = `${kibanaBase}/managing-indices.html`;
diff --git a/x-pack/legacy/plugins/index_management/public/register_routes.js b/x-pack/legacy/plugins/index_management/public/register_routes.js
index 41787befd5c23..0bbb9c0f56a61 100644
--- a/x-pack/legacy/plugins/index_management/public/register_routes.js
+++ b/x-pack/legacy/plugins/index_management/public/register_routes.js
@@ -7,7 +7,6 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Provider } from 'react-redux';
-import { i18n } from '@kbn/i18n';
import { setHttpClient } from './services/api';
import { setUrlService } from './services/navigation';
@@ -16,7 +15,6 @@ import { BASE_PATH } from '../common/constants/base_path';
import routes from 'ui/routes';
import { I18nContext } from 'ui/i18n';
-import { MANAGEMENT_BREADCRUMB } from 'ui/management';
import template from './main.html';
import { manageAngularLifecycle } from './lib/manage_angular_lifecycle';
@@ -35,15 +33,7 @@ const renderReact = async (elem) => {
};
routes.when(`${BASE_PATH}:view?/:action?/:id?`, {
- template: template,
- k7Breadcrumbs: () => [
- MANAGEMENT_BREADCRUMB,
- {
- text: i18n.translate('xpack.idxMgmt.breadcrumb', {
- defaultMessage: 'Index management'
- }),
- }
- ],
+ template,
controllerAs: 'indexManagement',
controller: class IndexManagementController {
constructor($scope, $route, $http, kbnUrl, $rootScope) {
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx
index 5d2fb54216da7..a533f57c914b9 100644
--- a/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -21,7 +21,8 @@ import {
import { BASE_PATH } from '../../../common/constants';
import { idxMgmtDocumentationLink } from '../../lib/documentation_links';
import { IndexList } from './index_list';
-import { TemplatesList } from './templates_list';
+import { TemplateList } from './template_list';
+import { setBreadcrumbs } from '../../services/set_breadcrumbs';
type Section = 'indices' | 'templates';
@@ -55,6 +56,10 @@ export const IndexManagementHome: React.FunctionComponent {
+ setBreadcrumbs();
+ }, []);
+
return (
@@ -92,7 +97,7 @@ export const IndexManagementHome: React.FunctionComponent onSectionChange(tab.id)}
isSelected={tab.id === section}
key={tab.id}
- data-test-subj="tab"
+ data-test-subj={`${tab.id}Tab`}
>
{tab.name}
@@ -104,7 +109,7 @@ export const IndexManagementHome: React.FunctionComponent
-
+
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/index.ts b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/index.ts
similarity index 82%
rename from x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/index.ts
rename to x-pack/legacy/plugins/index_management/public/sections/home/template_list/index.ts
index 89d2be3fa7e37..e53eedc6f2cb0 100644
--- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/index.ts
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { TemplatesTable } from './templates_table';
+export { TemplateList } from './template_list';
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/index.ts b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/index.ts
similarity index 100%
rename from x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/index.ts
rename to x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/index.ts
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/index.ts b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/index.ts
similarity index 57%
rename from x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/index.ts
rename to x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/index.ts
index a648a7f476312..7af28f4688f48 100644
--- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/index.ts
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/index.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { SummaryTab } from './summary_tab';
-export { MappingsTab } from './mappings_tab';
-export { SettingsTab } from './settings_tab';
-export { AliasesTab } from './aliases_tab';
+export { TabSummary } from './tab_summary';
+export { TabMappings } from './tab_mappings';
+export { TabSettings } from './tab_settings';
+export { TabAliases } from './tab_aliases';
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_aliases.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_aliases.tsx
new file mode 100644
index 0000000000000..d23a3f7be5fba
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_aliases.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
+import { Template } from '../../../../../../common/types';
+
+interface Props {
+ templateDetails: Template;
+}
+
+export const TabAliases: React.FunctionComponent = ({ templateDetails }) => {
+ const { aliases } = templateDetails;
+
+ if (aliases) {
+ return (
+
+ {aliases}
+
+ );
+ }
+
+ return (
+
+ }
+ iconType="pin"
+ data-test-subj="noAliasesCallout"
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_mappings.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_mappings.tsx
new file mode 100644
index 0000000000000..4711764b09727
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_mappings.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
+import { Template } from '../../../../../../common/types';
+
+interface Props {
+ templateDetails: Template;
+}
+
+export const TabMappings: React.FunctionComponent = ({ templateDetails }) => {
+ const { mappings } = templateDetails;
+
+ if (mappings) {
+ return (
+
+ {mappings}
+
+ );
+ }
+
+ return (
+
+ }
+ iconType="pin"
+ data-test-subj="noMappingsCallout"
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_settings.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_settings.tsx
new file mode 100644
index 0000000000000..06576109c76a9
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_settings.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
+import { Template } from '../../../../../../common/types';
+
+interface Props {
+ templateDetails: Template;
+}
+
+export const TabSettings: React.FunctionComponent = ({ templateDetails }) => {
+ const { settings } = templateDetails;
+
+ if (settings) {
+ return (
+
+ {settings}
+
+ );
+ }
+
+ return (
+
+ }
+ iconType="pin"
+ data-test-subj="noSettingsCallout"
+ >
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_summary.tsx
new file mode 100644
index 0000000000000..08e2a12bf6ad4
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/tabs/tab_summary.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiDescriptionList,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
+ EuiLink,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import { Template } from '../../../../../../common/types';
+import { getILMPolicyPath } from '../../../../../services/navigation';
+
+interface Props {
+ templateDetails: Template;
+}
+
+const NoneDescriptionText = () => (
+
+);
+
+export const TabSummary: React.FunctionComponent = ({ templateDetails }) => {
+ const { version, order, indexPatterns = [], ilmPolicy } = templateDetails;
+
+ const numIndexPatterns = indexPatterns.length;
+
+ return (
+
+
+
+
+
+ {numIndexPatterns > 1 ? (
+
+
+ {indexPatterns.map((indexName: string, i: number) => {
+ return (
+ -
+
+ {indexName}
+
+
+ );
+ })}
+
+
+ ) : (
+ indexPatterns.toString()
+ )}
+
+
+
+
+
+ {ilmPolicy && ilmPolicy.name ? (
+ {ilmPolicy.name}
+ ) : (
+
+ )}
+
+
+
+
+
+ {order || order === 0 ? order : }
+
+
+
+
+
+ {version || version === 0 ? version : }
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/template_details.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/template_details.tsx
new file mode 100644
index 0000000000000..22f8d2e385060
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_details/template_details.tsx
@@ -0,0 +1,282 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment, useState } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiTitle,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiTab,
+ EuiTabs,
+ EuiSpacer,
+ EuiPopover,
+ EuiButton,
+ EuiContextMenu,
+} from '@elastic/eui';
+import {
+ UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
+ UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
+ UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
+ UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
+} from '../../../../../common/constants';
+import { Template } from '../../../../../common/types';
+import { TemplateDeleteModal, SectionLoading, SectionError } from '../../../../components';
+import { loadIndexTemplate } from '../../../../services/api';
+import { decodePath } from '../../../../services/routing';
+import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric';
+import { TabSummary, TabMappings, TabSettings, TabAliases } from './tabs';
+
+interface Props {
+ templateName: Template['name'];
+ onClose: () => void;
+ editTemplate: (templateName: Template['name']) => void;
+ cloneTemplate: (templateName: Template['name']) => void;
+ reload: () => Promise;
+}
+
+const SUMMARY_TAB_ID = 'summary';
+const MAPPINGS_TAB_ID = 'mappings';
+const ALIASES_TAB_ID = 'aliases';
+const SETTINGS_TAB_ID = 'settings';
+
+const TABS = [
+ {
+ id: SUMMARY_TAB_ID,
+ name: i18n.translate('xpack.idxMgmt.templateDetails.summaryTabTitle', {
+ defaultMessage: 'Summary',
+ }),
+ },
+ {
+ id: SETTINGS_TAB_ID,
+ name: i18n.translate('xpack.idxMgmt.templateDetails.settingsTabTitle', {
+ defaultMessage: 'Settings',
+ }),
+ },
+ {
+ id: MAPPINGS_TAB_ID,
+ name: i18n.translate('xpack.idxMgmt.templateDetails.mappingsTabTitle', {
+ defaultMessage: 'Mappings',
+ }),
+ },
+ {
+ id: ALIASES_TAB_ID,
+ name: i18n.translate('xpack.idxMgmt.templateDetails.aliasesTabTitle', {
+ defaultMessage: 'Aliases',
+ }),
+ },
+];
+
+const tabToComponentMap: {
+ [key: string]: React.FunctionComponent<{ templateDetails: Template }>;
+} = {
+ [SUMMARY_TAB_ID]: TabSummary,
+ [SETTINGS_TAB_ID]: TabSettings,
+ [MAPPINGS_TAB_ID]: TabMappings,
+ [ALIASES_TAB_ID]: TabAliases,
+};
+
+const tabToUiMetricMap: { [key: string]: string } = {
+ [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
+ [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
+ [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
+ [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
+};
+
+export const TemplateDetails: React.FunctionComponent = ({
+ templateName,
+ onClose,
+ editTemplate,
+ cloneTemplate,
+ reload,
+}) => {
+ const decodedTemplateName = decodePath(templateName);
+ const { error, data: templateDetails, isLoading } = loadIndexTemplate(decodedTemplateName);
+ const [templateToDelete, setTemplateToDelete] = useState>([]);
+ const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID);
+ const [isPopoverOpen, setIsPopOverOpen] = useState(false);
+
+ const contextMenuItems = [
+ {
+ name: i18n.translate('xpack.idxMgmt.templateDetails.editButtonLabel', {
+ defaultMessage: 'Edit',
+ }),
+ icon: 'pencil',
+ onClick: () => editTemplate(decodedTemplateName),
+ },
+ {
+ name: i18n.translate('xpack.idxMgmt.templateDetails.cloneButtonLabel', {
+ defaultMessage: 'Clone',
+ }),
+ icon: 'copy',
+ onClick: () => cloneTemplate(decodedTemplateName),
+ },
+ {
+ name: i18n.translate('xpack.idxMgmt.templateDetails.deleteButtonLabel', {
+ defaultMessage: 'Delete',
+ }),
+ icon: 'trash',
+ onClick: () => setTemplateToDelete([decodedTemplateName]),
+ },
+ ];
+
+ let content;
+
+ if (isLoading) {
+ content = (
+
+
+
+ );
+ } else if (error) {
+ content = (
+
+ }
+ error={error}
+ data-test-subj="sectionError"
+ />
+ );
+ } else if (templateDetails) {
+ const Content = tabToComponentMap[activeTab];
+
+ content = (
+
+
+ {TABS.map(tab => (
+ {
+ trackUiMetric(METRIC_TYPE.CLICK, tabToUiMetricMap[tab.id]);
+ setActiveTab(tab.id);
+ }}
+ isSelected={tab.id === activeTab}
+ key={tab.id}
+ data-test-subj="tab"
+ >
+ {tab.name}
+
+ ))}
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {templateToDelete && templateToDelete.length > 0 ? (
+ {
+ if (data && data.hasDeletedTemplates) {
+ reload();
+ } else {
+ setTemplateToDelete([]);
+ }
+ onClose();
+ }}
+ templatesToDelete={templateToDelete}
+ />
+ ) : null}
+
+
+
+
+
+ {decodedTemplateName}
+
+
+
+
+ {content}
+
+
+
+
+
+
+
+
+
+ {templateDetails && (
+
+ {/* Manage templates context menu */}
+ setIsPopOverOpen(prev => !prev)}
+ >
+
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={() => setIsPopOverOpen(false)}
+ panelPaddingSize="none"
+ withTitle
+ anchorPosition="rightUp"
+ repositionOnScroll
+ >
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_list.tsx
similarity index 74%
rename from x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx
rename to x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_list.tsx
index 51fa92582d573..78e2cb164b665 100644
--- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_list.tsx
@@ -17,24 +17,29 @@ import {
EuiFlexGroup,
} from '@elastic/eui';
import { SectionError, SectionLoading } from '../../../components';
-import { TemplatesTable } from './templates_table';
+import { TemplateTable } from './template_table';
import { loadIndexTemplates } from '../../../services/api';
import { Template } from '../../../../common/types';
import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric';
-import { UIM_TEMPLATE_LIST_LOAD, BASE_PATH } from '../../../../common/constants';
+import {
+ getTemplateEditLink,
+ getTemplateListLink,
+ getTemplateCloneLink,
+} from '../../../services/routing';
+import { UIM_TEMPLATE_LIST_LOAD } from '../../../../common/constants';
import { TemplateDetails } from './template_details';
interface MatchParams {
templateName?: Template['name'];
}
-export const TemplatesList: React.FunctionComponent> = ({
+export const TemplateList: React.FunctionComponent> = ({
match: {
params: { templateName },
},
history,
}) => {
- const { error, isLoading, data: templates, createRequest: reload } = loadIndexTemplates();
+ const { error, isLoading, data: templates, sendRequest: reload } = loadIndexTemplates();
let content;
@@ -48,7 +53,15 @@ export const TemplatesList: React.FunctionComponent {
- history.push(`${BASE_PATH}templates`);
+ history.push(getTemplateListLink());
+ };
+
+ const editTemplate = (name: Template['name']) => {
+ history.push(getTemplateEditLink(name));
+ };
+
+ const cloneTemplate = (name: Template['name']) => {
+ history.push(getTemplateCloneLink(name));
};
// Track component loaded
@@ -61,7 +74,7 @@ export const TemplatesList: React.FunctionComponent
);
@@ -71,7 +84,7 @@ export const TemplatesList: React.FunctionComponent
}
error={error}
@@ -85,7 +98,7 @@ export const TemplatesList: React.FunctionComponent
}
@@ -101,7 +114,7 @@ export const TemplatesList: React.FunctionComponent
@@ -115,31 +128,35 @@ export const TemplatesList: React.FunctionComponent
}
/>
-
);
}
return (
-
+
{content}
{templateName && (
)}
-
+
);
};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_table/index.ts b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_table/index.ts
new file mode 100644
index 0000000000000..b347725df43ad
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_table/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { TemplateTable } from './template_table';
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_table/template_table.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_table/template_table.tsx
new file mode 100644
index 0000000000000..c018c11b36ff4
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/home/template_list/template_table/template_table.tsx
@@ -0,0 +1,271 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState, Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink } from '@elastic/eui';
+import { TemplateListItem, Template } from '../../../../../common/types';
+import { BASE_PATH, UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../common/constants';
+import { TemplateDeleteModal } from '../../../../components';
+import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric';
+import { getTemplateDetailsLink } from '../../../../services/routing';
+
+interface Props {
+ templates: TemplateListItem[];
+ reload: () => Promise;
+ editTemplate: (name: Template['name']) => void;
+ cloneTemplate: (name: Template['name']) => void;
+}
+
+export const TemplateTable: React.FunctionComponent = ({
+ templates,
+ reload,
+ editTemplate,
+ cloneTemplate,
+}) => {
+ const [selection, setSelection] = useState([]);
+ const [templatesToDelete, setTemplatesToDelete] = useState>([]);
+
+ const columns = [
+ {
+ field: 'name',
+ name: i18n.translate('xpack.idxMgmt.templateList.table.nameColumnTitle', {
+ defaultMessage: 'Name',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (name: TemplateListItem['name']) => {
+ return (
+ /* eslint-disable-next-line @elastic/eui/href-or-on-click */
+ trackUiMetric(METRIC_TYPE.CLICK, UIM_TEMPLATE_SHOW_DETAILS_CLICK)}
+ >
+ {name}
+
+ );
+ },
+ },
+ {
+ field: 'indexPatterns',
+ name: i18n.translate('xpack.idxMgmt.templateList.table.indexPatternsColumnTitle', {
+ defaultMessage: 'Index patterns',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (indexPatterns: string[]) => {indexPatterns.join(', ')},
+ },
+ {
+ field: 'ilmPolicy',
+ name: i18n.translate('xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle', {
+ defaultMessage: 'ILM policy',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (ilmPolicy: { name: string }) =>
+ ilmPolicy && ilmPolicy.name ? (
+
+ {ilmPolicy.name}
+
+ ) : null,
+ },
+ {
+ field: 'order',
+ name: i18n.translate('xpack.idxMgmt.templateList.table.orderColumnTitle', {
+ defaultMessage: 'Order',
+ }),
+ truncateText: true,
+ sortable: true,
+ },
+ {
+ field: 'hasMappings',
+ name: i18n.translate('xpack.idxMgmt.templateList.table.mappingsColumnTitle', {
+ defaultMessage: 'Mappings',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (hasMappings: boolean) => (hasMappings ? : null),
+ },
+ {
+ field: 'hasSettings',
+ name: i18n.translate('xpack.idxMgmt.templateList.table.settingsColumnTitle', {
+ defaultMessage: 'Settings',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (hasSettings: boolean) => (hasSettings ? : null),
+ },
+ {
+ field: 'hasAliases',
+ name: i18n.translate('xpack.idxMgmt.templateList.table.aliasesColumnTitle', {
+ defaultMessage: 'Aliases',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (hasAliases: boolean) => (hasAliases ? : null),
+ },
+ {
+ name: i18n.translate('xpack.idxMgmt.templateList.table.actionColumnTitle', {
+ defaultMessage: 'Actions',
+ }),
+ actions: [
+ {
+ name: i18n.translate('xpack.idxMgmt.templateList.table.actionEditText', {
+ defaultMessage: 'Edit',
+ }),
+ isPrimary: true,
+ description: i18n.translate('xpack.idxMgmt.templateList.table.actionEditDecription', {
+ defaultMessage: 'Edit this template',
+ }),
+ icon: 'pencil',
+ type: 'icon',
+ onClick: ({ name }: Template) => {
+ editTemplate(name);
+ },
+ },
+ {
+ name: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneTitle', {
+ defaultMessage: 'Clone',
+ }),
+ description: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneDescription', {
+ defaultMessage: 'Clone this template',
+ }),
+ icon: 'copy',
+ onClick: ({ name }: Template) => {
+ cloneTemplate(name);
+ },
+ },
+ {
+ name: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteText', {
+ defaultMessage: 'Delete',
+ }),
+ description: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteDecription', {
+ defaultMessage: 'Delete this template',
+ }),
+ icon: 'trash',
+ color: 'danger',
+ type: 'icon',
+ onClick: ({ name }: Template) => {
+ setTemplatesToDelete([name]);
+ },
+ isPrimary: true,
+ },
+ ],
+ },
+ ];
+
+ const pagination = {
+ initialPageSize: 20,
+ pageSizeOptions: [10, 20, 50],
+ };
+
+ const sorting = {
+ sort: {
+ field: 'name',
+ direction: 'asc',
+ },
+ };
+
+ const selectionConfig = {
+ onSelectionChange: setSelection,
+ };
+
+ const searchConfig = {
+ box: {
+ incremental: true,
+ },
+ toolsLeft: selection.length && (
+
+ setTemplatesToDelete(selection.map((selected: TemplateListItem) => selected.name))
+ }
+ color="danger"
+ >
+
+
+ ),
+ toolsRight: [
+
+
+ ,
+
+
+ ,
+ ],
+ };
+
+ return (
+
+ {templatesToDelete && templatesToDelete.length > 0 ? (
+ {
+ if (data && data.hasDeletedTemplates) {
+ reload();
+ } else {
+ setTemplatesToDelete([]);
+ }
+ }}
+ templatesToDelete={templatesToDelete}
+ />
+ ) : null}
+ ({
+ 'data-test-subj': 'row',
+ })}
+ cellProps={() => ({
+ 'data-test-subj': 'cell',
+ })}
+ data-test-subj="templateTable"
+ message={
+
+ }
+ />
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/aliases_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/aliases_tab.tsx
deleted file mode 100644
index 02cb59619aae1..0000000000000
--- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/aliases_tab.tsx
+++ /dev/null
@@ -1,37 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { EuiCodeEditor } from '@elastic/eui';
-import { Template } from '../../../../../../common/types';
-
-interface Props {
- templateDetails: Template;
-}
-
-export const AliasesTab: React.FunctionComponent = ({ templateDetails }) => {
- const { aliases } = templateDetails;
- const aliasesJsonString = JSON.stringify(aliases, null, 2);
-
- return (
-
-
-
- );
-};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/mappings_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/mappings_tab.tsx
deleted file mode 100644
index 15133e595a280..0000000000000
--- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/mappings_tab.tsx
+++ /dev/null
@@ -1,37 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { EuiCodeEditor } from '@elastic/eui';
-import { Template } from '../../../../../../common/types';
-
-interface Props {
- templateDetails: Template;
-}
-
-export const MappingsTab: React.FunctionComponent = ({ templateDetails }) => {
- const { mappings } = templateDetails;
- const mappingsJsonString = JSON.stringify(mappings, null, 2);
-
- return (
-
-
-
- );
-};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/settings_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/settings_tab.tsx
deleted file mode 100644
index 697c2e4ab5272..0000000000000
--- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/settings_tab.tsx
+++ /dev/null
@@ -1,37 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { EuiCodeEditor } from '@elastic/eui';
-import { Template } from '../../../../../../common/types';
-
-interface Props {
- templateDetails: Template;
-}
-
-export const SettingsTab: React.FunctionComponent = ({ templateDetails }) => {
- const { settings } = templateDetails;
- const settingsJsonString = JSON.stringify(settings, null, 2);
-
- return (
-
-
-
- );
-};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/summary_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/summary_tab.tsx
deleted file mode 100644
index 3d55bdf6ab977..0000000000000
--- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/summary_tab.tsx
+++ /dev/null
@@ -1,109 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiDescriptionList,
- EuiDescriptionListTitle,
- EuiDescriptionListDescription,
- EuiLink,
- EuiFlexGroup,
- EuiFlexItem,
- EuiText,
- EuiTitle,
-} from '@elastic/eui';
-import { Template } from '../../../../../../common/types';
-import { getILMPolicyPath } from '../../../../../services/navigation';
-
-interface Props {
- templateDetails: Template;
-}
-
-const NoneDescriptionText = () => (
-
-);
-
-export const SummaryTab: React.FunctionComponent = ({ templateDetails }) => {
- const { version, order, indexPatterns = [], settings } = templateDetails;
-
- const ilmPolicy = settings && settings.index && settings.index.lifecycle;
- const numIndexPatterns = indexPatterns.length;
-
- return (
-
-
-
-
-
-
-
- {numIndexPatterns > 1 ? (
-
-
- {indexPatterns.map((indexName: string, i: number) => {
- return (
- -
-
- {indexName}
-
-
- );
- })}
-
-
- ) : (
- indexPatterns.toString()
- )}
-
-
-
-
-
-
-
-
-
-
- {ilmPolicy && ilmPolicy.name ? (
- {ilmPolicy.name}
- ) : (
-
- )}
-
-
-
-
-
- {typeof order !== 'undefined' ? order : }
-
-
-
-
-
- {typeof version !== 'undefined' ? version : }
-
-
-
-
- );
-};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/template_details.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/template_details.tsx
deleted file mode 100644
index 9c295fbcbc2d3..0000000000000
--- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/template_details.tsx
+++ /dev/null
@@ -1,256 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { Fragment, useState } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiFlyout,
- EuiFlyoutHeader,
- EuiTitle,
- EuiFlyoutBody,
- EuiFlyoutFooter,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiTab,
- EuiTabs,
- EuiSpacer,
-} from '@elastic/eui';
-import {
- UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
- UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
- UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
- UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
-} from '../../../../../common/constants';
-import { Template } from '../../../../../common/types';
-import { DeleteTemplatesModal, SectionLoading, SectionError } from '../../../../components';
-import { loadIndexTemplate } from '../../../../services/api';
-import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric';
-import { SummaryTab, MappingsTab, SettingsTab, AliasesTab } from './tabs';
-
-interface Props {
- templateName: Template['name'];
- onClose: () => void;
- reload: () => Promise;
-}
-
-const SUMMARY_TAB_ID = 'summary';
-const MAPPINGS_TAB_ID = 'mappings';
-const ALIASES_TAB_ID = 'aliases';
-const SETTINGS_TAB_ID = 'settings';
-
-const summaryTabData = {
- id: SUMMARY_TAB_ID,
- name: (
-
- ),
-};
-
-const settingsTabData = {
- id: SETTINGS_TAB_ID,
- name: (
-
- ),
-};
-
-const mappingsTabData = {
- id: MAPPINGS_TAB_ID,
- name: (
-
- ),
-};
-
-const aliasesTabData = {
- id: ALIASES_TAB_ID,
- name: (
-
- ),
-};
-
-const tabToComponentMap: {
- [key: string]: React.FunctionComponent<{ templateDetails: Template }>;
-} = {
- [SUMMARY_TAB_ID]: SummaryTab,
- [SETTINGS_TAB_ID]: SettingsTab,
- [MAPPINGS_TAB_ID]: MappingsTab,
- [ALIASES_TAB_ID]: AliasesTab,
-};
-
-const tabToUiMetricMap: { [key: string]: string } = {
- [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
- [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
- [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
- [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
-};
-
-const hasEntries = (tabData: object) => {
- return tabData ? Object.entries(tabData).length > 0 : false;
-};
-
-export const TemplateDetails: React.FunctionComponent = ({
- templateName,
- onClose,
- reload,
-}) => {
- const { error, data: templateDetails, isLoading } = loadIndexTemplate(templateName);
-
- const [templateToDelete, setTemplateToDelete] = useState>([]);
- const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID);
-
- let content;
-
- if (isLoading) {
- content = (
-
-
-
- );
- } else if (error) {
- content = (
-
- }
- error={error}
- data-test-subj="sectionError"
- />
- );
- } else if (templateDetails) {
- const { settings, mappings, aliases } = templateDetails;
-
- const settingsTab = hasEntries(settings) ? [settingsTabData] : [];
- const mappingsTab = hasEntries(mappings) ? [mappingsTabData] : [];
- const aliasesTab = hasEntries(aliases) ? [aliasesTabData] : [];
-
- const optionalTabs = [...settingsTab, ...mappingsTab, ...aliasesTab];
- const tabs = optionalTabs.length > 0 ? [summaryTabData, ...optionalTabs] : [];
-
- if (tabs.length > 0) {
- const Content = tabToComponentMap[activeTab];
-
- content = (
-
-
- {tabs.map(tab => (
- {
- trackUiMetric(METRIC_TYPE.CLICK, tabToUiMetricMap[tab.id]);
- setActiveTab(tab.id);
- }}
- isSelected={tab.id === activeTab}
- key={tab.id}
- data-test-subj="tab"
- >
- {tab.name}
-
- ))}
-
-
-
-
-
-
- );
- } else {
- content = (
-
-
-
-
-
-
-
-
-
-
-
- );
- }
- }
-
- return (
-
- {templateToDelete.length ? (
- {
- if (data && data.hasDeletedTemplates) {
- reload();
- }
- setTemplateToDelete([]);
- onClose();
- }}
- templatesToDelete={templateToDelete}
- />
- ) : null}
-
-
-
-
-
- {templateName}
-
-
-
-
- {content}
-
-
-
-
-
-
-
-
-
- {templateDetails && (
-
-
-
-
-
- )}
-
-
-
-
- );
-};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx
deleted file mode 100644
index d52486ba4bde8..0000000000000
--- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx
+++ /dev/null
@@ -1,266 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { useState, Fragment } from 'react';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiInMemoryTable,
- EuiIcon,
- EuiButton,
- EuiToolTip,
- EuiButtonIcon,
- EuiLink,
-} from '@elastic/eui';
-import { Template } from '../../../../../common/types';
-import { BASE_PATH, UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../common/constants';
-import { DeleteTemplatesModal } from '../../../../components';
-import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric';
-
-interface Props {
- templates: Template[];
- reload: () => Promise;
-}
-
-const Checkmark = ({ tableCellData }: { tableCellData: object }) => {
- const isChecked = Object.entries(tableCellData).length > 0;
-
- return isChecked ? : null;
-};
-
-export const TemplatesTable: React.FunctionComponent = ({ templates, reload }) => {
- const [selection, setSelection] = useState([]);
- const [templatesToDelete, setTemplatesToDelete] = useState>([]);
-
- const columns = [
- {
- field: 'name',
- name: i18n.translate('xpack.idxMgmt.templatesList.table.nameColumnTitle', {
- defaultMessage: 'Name',
- }),
- truncateText: true,
- sortable: true,
- render: (name: Template['name']) => {
- return (
- /* eslint-disable-next-line @elastic/eui/href-or-on-click */
- trackUiMetric(METRIC_TYPE.CLICK, UIM_TEMPLATE_SHOW_DETAILS_CLICK)}
- >
- {name}
-
- );
- },
- },
- {
- field: 'indexPatterns',
- name: i18n.translate('xpack.idxMgmt.templatesList.table.indexPatternsColumnTitle', {
- defaultMessage: 'Index patterns',
- }),
- truncateText: true,
- sortable: true,
- render: (indexPatterns: string[]) => {indexPatterns.join(', ')},
- },
- {
- field: 'settings',
- name: i18n.translate('xpack.idxMgmt.templatesList.table.ilmPolicyColumnTitle', {
- defaultMessage: 'ILM policy',
- }),
- truncateText: true,
- sortable: true,
- render: (settings?: {
- index: {
- lifecycle: {
- name: string;
- };
- };
- }) => {
- if (settings && settings.index && settings.index.lifecycle) {
- return settings.index.lifecycle.name;
- }
- return null;
- },
- },
- {
- field: 'order',
- name: i18n.translate('xpack.idxMgmt.templatesList.table.orderColumnTitle', {
- defaultMessage: 'Order',
- }),
- truncateText: true,
- sortable: true,
- },
- {
- field: 'mappings',
- name: i18n.translate('xpack.idxMgmt.templatesList.table.mappingsColumnTitle', {
- defaultMessage: 'Mappings',
- }),
- truncateText: true,
- sortable: true,
- width: '100px',
- render: (mappings: object) => ,
- },
- {
- field: 'settings',
- name: i18n.translate('xpack.idxMgmt.templatesList.table.settingsColumnTitle', {
- defaultMessage: 'Settings',
- }),
- truncateText: true,
- sortable: true,
- width: '100px',
- render: (settings: object) => ,
- },
- {
- field: 'aliases',
- name: i18n.translate('xpack.idxMgmt.templatesList.table.aliasesColumnTitle', {
- defaultMessage: 'Aliases',
- }),
- truncateText: true,
- sortable: true,
- width: '100px',
- render: (aliases: object) => {
- return ;
- },
- },
- {
- name: i18n.translate('xpack.idxMgmt.templatesList.table.actionColumnTitle', {
- defaultMessage: 'Actions',
- }),
- width: '75px',
- actions: [
- {
- render: (template: Template) => {
- const { name } = template;
-
- return (
-
- {
- setTemplatesToDelete([name]);
- }}
- data-test-subj="deleteTemplateButton"
- />
-
- );
- },
- },
- ],
- },
- ];
-
- const pagination = {
- initialPageSize: 20,
- pageSizeOptions: [10, 20, 50],
- };
-
- const sorting = {
- sort: {
- field: 'name',
- direction: 'asc',
- },
- };
-
- const selectionConfig = {
- onSelectionChange: setSelection,
- };
-
- const searchConfig = {
- box: {
- incremental: true,
- },
- filters: [
- {
- type: 'is',
- field: 'settings.index.lifecycle.name',
- name: i18n.translate('xpack.idxMgmt.templatesList.table.ilmPolicyFilterLabel', {
- defaultMessage: 'ILM policy',
- }),
- },
- ],
- toolsLeft: selection.length && (
- setTemplatesToDelete(selection.map((selected: Template) => selected.name))}
- color="danger"
- >
-
-
- ),
- toolsRight: (
-
-
-
- ),
- };
-
- return (
-
- {templatesToDelete.length ? (
- {
- if (data && data.hasDeletedTemplates) {
- reload();
- }
- setTemplatesToDelete([]);
- }}
- templatesToDelete={templatesToDelete}
- />
- ) : null}
- ({
- 'data-test-subj': 'row',
- })}
- cellProps={() => ({
- 'data-test-subj': 'cell',
- })}
- data-test-subj="templatesTable"
- message={
-
- }
- />
-
- );
-};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/template_clone/index.ts b/x-pack/legacy/plugins/index_management/public/sections/template_clone/index.ts
new file mode 100644
index 0000000000000..8edace200e912
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/template_clone/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { TemplateClone } from './template_clone';
diff --git a/x-pack/legacy/plugins/index_management/public/sections/template_clone/template_clone.tsx b/x-pack/legacy/plugins/index_management/public/sections/template_clone/template_clone.tsx
new file mode 100644
index 0000000000000..91f737ca36ad2
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/template_clone/template_clone.tsx
@@ -0,0 +1,118 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useEffect, useState } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { TemplateForm, SectionLoading, SectionError } from '../../components';
+import { setBreadcrumbs } from '../../services/set_breadcrumbs';
+import { decodePath, getTemplateDetailsLink } from '../../services/routing';
+import { Template } from '../../../common/types';
+import { saveTemplate, loadIndexTemplate } from '../../services/api';
+
+interface MatchParams {
+ name: string;
+}
+
+export const TemplateClone: React.FunctionComponent> = ({
+ match: {
+ params: { name },
+ },
+ history,
+}) => {
+ const decodedTemplateName = decodePath(name);
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState(null);
+
+ const { error: templateToCloneError, data: templateToClone, isLoading } = loadIndexTemplate(
+ decodedTemplateName
+ );
+
+ const onSave = async (template: Template) => {
+ setIsSaving(true);
+ setSaveError(null);
+
+ const { error } = await saveTemplate(template, true);
+
+ const { name: newTemplateName } = template;
+
+ setIsSaving(false);
+
+ if (error) {
+ setSaveError(error);
+ return;
+ }
+
+ history.push(getTemplateDetailsLink(newTemplateName));
+ };
+
+ const clearSaveError = () => {
+ setSaveError(null);
+ };
+
+ let content;
+
+ useEffect(() => {
+ setBreadcrumbs('templateClone');
+ }, []);
+
+ if (isLoading) {
+ content = (
+
+
+
+ );
+ } else if (templateToCloneError) {
+ content = (
+
+ }
+ error={templateToCloneError}
+ data-test-subj="sectionError"
+ />
+ );
+ } else if (templateToClone) {
+ const templateData = {
+ ...templateToClone,
+ ...{ name: `${decodedTemplateName}-copy` },
+ } as Template;
+
+ content = (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {content}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/template_create/index.ts b/x-pack/legacy/plugins/index_management/public/sections/template_create/index.ts
new file mode 100644
index 0000000000000..ad8d880d71e45
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/template_create/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { TemplateCreate } from './template_create';
diff --git a/x-pack/legacy/plugins/index_management/public/sections/template_create/template_create.tsx b/x-pack/legacy/plugins/index_management/public/sections/template_create/template_create.tsx
new file mode 100644
index 0000000000000..5d15ed2718e2c
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/template_create/template_create.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useEffect, useState } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { TemplateForm } from '../../components';
+import { setBreadcrumbs } from '../../services/set_breadcrumbs';
+import { Template } from '../../../common/types';
+import { saveTemplate } from '../../services/api';
+import { getTemplateDetailsLink } from '../../services/routing';
+
+const emptyObject = '{\n\n}';
+
+const DEFAULT_TEMPLATE: Template = {
+ name: '',
+ indexPatterns: [],
+ version: '',
+ order: '',
+ settings: emptyObject,
+ mappings: emptyObject,
+ aliases: emptyObject,
+};
+
+export const TemplateCreate: React.FunctionComponent = ({ history }) => {
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState(null);
+
+ const onSave = async (template: Template) => {
+ const { name } = template;
+
+ setIsSaving(true);
+ setSaveError(null);
+
+ const { error } = await saveTemplate(template);
+
+ setIsSaving(false);
+
+ if (error) {
+ setSaveError(error);
+ return;
+ }
+
+ history.push(getTemplateDetailsLink(name));
+ };
+
+ const clearSaveError = () => {
+ setSaveError(null);
+ };
+
+ useEffect(() => {
+ setBreadcrumbs('templateCreate');
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/sections/template_edit/index.ts b/x-pack/legacy/plugins/index_management/public/sections/template_edit/index.ts
new file mode 100644
index 0000000000000..f4efde42bb540
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/template_edit/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { TemplateEdit } from './template_edit';
diff --git a/x-pack/legacy/plugins/index_management/public/sections/template_edit/template_edit.tsx b/x-pack/legacy/plugins/index_management/public/sections/template_edit/template_edit.tsx
new file mode 100644
index 0000000000000..737e65f83f91d
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/sections/template_edit/template_edit.tsx
@@ -0,0 +1,136 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useEffect, useState, Fragment } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
+import { setBreadcrumbs } from '../../services/set_breadcrumbs';
+import { loadIndexTemplate, updateTemplate } from '../../services/api';
+import { decodePath, getTemplateDetailsLink } from '../../services/routing';
+import { SectionLoading, SectionError, TemplateForm } from '../../components';
+import { Template } from '../../../common/types';
+
+interface MatchParams {
+ name: string;
+}
+
+export const TemplateEdit: React.FunctionComponent> = ({
+ match: {
+ params: { name },
+ },
+ history,
+}) => {
+ const decodedTemplateName = decodePath(name);
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState(null);
+
+ const { error, data: template, isLoading } = loadIndexTemplate(decodedTemplateName);
+
+ useEffect(() => {
+ setBreadcrumbs('templateEdit');
+ }, []);
+
+ const onSave = async (updatedTemplate: Template) => {
+ setIsSaving(true);
+ setSaveError(null);
+
+ const { error: saveErrorObject } = await updateTemplate(updatedTemplate);
+
+ setIsSaving(false);
+
+ if (saveErrorObject) {
+ setSaveError(saveErrorObject);
+ return;
+ }
+
+ history.push(getTemplateDetailsLink(name));
+ };
+
+ const clearSaveError = () => {
+ setSaveError(null);
+ };
+
+ let content;
+
+ if (isLoading) {
+ content = (
+
+
+
+ );
+ } else if (error) {
+ content = (
+
+ }
+ error={error}
+ data-test-subj="sectionError"
+ />
+ );
+ } else if (template) {
+ const { name: templateName } = template;
+ const isSystemTemplate = templateName && templateName.startsWith('.');
+
+ content = (
+
+ {isSystemTemplate && (
+
+
+ }
+ color="danger"
+ iconType="alert"
+ data-test-subj="systemTemplateEditCallout"
+ >
+
+
+
+
+ )}
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {content}
+
+
+ );
+};
diff --git a/x-pack/legacy/plugins/index_management/public/services/api.ts b/x-pack/legacy/plugins/index_management/public/services/api.ts
index 84cfc0487092c..4592381cad631 100644
--- a/x-pack/legacy/plugins/index_management/public/services/api.ts
+++ b/x-pack/legacy/plugins/index_management/public/services/api.ts
@@ -28,6 +28,9 @@ import {
UIM_INDEX_UNFREEZE_MANY,
UIM_TEMPLATE_DELETE,
UIM_TEMPLATE_DELETE_MANY,
+ UIM_TEMPLATE_CREATE,
+ UIM_TEMPLATE_UPDATE,
+ UIM_TEMPLATE_CLONE,
} from '../../common/constants';
import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants';
@@ -201,15 +204,18 @@ export function loadIndexTemplates() {
});
}
-export const deleteTemplates = async (names: Array) => {
- const uimActionType = names.length > 1 ? UIM_TEMPLATE_DELETE_MANY : UIM_TEMPLATE_DELETE;
-
- return sendRequest({
+export async function deleteTemplates(names: Array) {
+ const result = sendRequest({
path: `${apiPrefix}/templates/${names.map(name => encodeURIComponent(name)).join(',')}`,
method: 'delete',
- uimActionType,
});
-};
+
+ const uimActionType = names.length > 1 ? UIM_TEMPLATE_DELETE_MANY : UIM_TEMPLATE_DELETE;
+
+ trackUiMetric(METRIC_TYPE.COUNT, uimActionType);
+
+ return result;
+}
export function loadIndexTemplate(name: Template['name']) {
return useRequest({
@@ -217,3 +223,37 @@ export function loadIndexTemplate(name: Template['name']) {
method: 'get',
});
}
+
+export async function saveTemplate(template: Template, isClone?: boolean) {
+ const result = sendRequest({
+ path: `${apiPrefix}/templates`,
+ method: 'put',
+ body: template,
+ });
+
+ const uimActionType = isClone ? UIM_TEMPLATE_CLONE : UIM_TEMPLATE_CREATE;
+
+ trackUiMetric(METRIC_TYPE.COUNT, uimActionType);
+
+ return result;
+}
+
+export async function updateTemplate(template: Template) {
+ const { name } = template;
+ const result = sendRequest({
+ path: `${apiPrefix}/templates/${encodeURIComponent(name)}`,
+ method: 'put',
+ body: template,
+ });
+
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_TEMPLATE_UPDATE);
+
+ return result;
+}
+
+export async function loadTemplateToClone(name: Template['name']) {
+ return sendRequest({
+ path: `${apiPrefix}/templates/${encodeURIComponent(name)}`,
+ method: 'get',
+ });
+}
diff --git a/x-pack/legacy/plugins/index_management/public/services/routing.ts b/x-pack/legacy/plugins/index_management/public/services/routing.ts
new file mode 100644
index 0000000000000..a49e3950b5591
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/services/routing.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { BASE_PATH } from '../../common/constants';
+
+export const getTemplateListLink = () => {
+ return `${BASE_PATH}templates`;
+};
+
+// Need to add some additonal encoding/decoding logic to work with React Router
+// For background, see: https://github.com/ReactTraining/history/issues/505
+export const getTemplateDetailsLink = (name: string, withHash = false) => {
+ const baseUrl = `${BASE_PATH}templates/${encodeURIComponent(encodeURIComponent(name))}`;
+ const url = withHash ? `#${baseUrl}` : baseUrl;
+ return encodeURI(url);
+};
+
+export const getTemplateEditLink = (name: string) => {
+ return encodeURI(`${BASE_PATH}edit_template/${encodeURIComponent(encodeURIComponent(name))}`);
+};
+
+export const getTemplateCloneLink = (name: string) => {
+ return encodeURI(`${BASE_PATH}clone_template/${encodeURIComponent(encodeURIComponent(name))}`);
+};
+
+export const decodePath = (pathname: string): string => {
+ let decodedPath;
+ try {
+ decodedPath = decodeURI(pathname);
+ decodedPath = decodeURIComponent(decodedPath);
+ } catch (_error) {
+ decodedPath = decodeURIComponent(pathname);
+ }
+ return decodeURIComponent(decodedPath);
+};
diff --git a/x-pack/legacy/plugins/index_management/public/services/set_breadcrumbs.ts b/x-pack/legacy/plugins/index_management/public/services/set_breadcrumbs.ts
new file mode 100644
index 0000000000000..dd68b212286e7
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/services/set_breadcrumbs.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import chrome from 'ui/chrome';
+import { MANAGEMENT_BREADCRUMB } from 'ui/management';
+import { i18n } from '@kbn/i18n';
+import { BASE_PATH } from '../../common/constants';
+
+interface Breadcrumb {
+ text: string;
+ href?: string;
+}
+
+const homeBreadcrumb: Breadcrumb = {
+ text: i18n.translate('xpack.idxMgmt.breadcrumb.homeLabel', {
+ defaultMessage: 'Index Management',
+ }),
+ href: `#${BASE_PATH}`,
+};
+
+const templatesBreadcrumb: Breadcrumb = {
+ text: i18n.translate('xpack.idxMgmt.breadcrumb.templatesLabel', {
+ defaultMessage: 'Templates',
+ }),
+ href: `#${BASE_PATH}templates`,
+};
+
+const breadcrumbsMap: {
+ [key: string]: Breadcrumb;
+} = {
+ templateCreate: {
+ text: i18n.translate('xpack.idxMgmt.breadcrumb.createTemplateLabel', {
+ defaultMessage: 'Create template',
+ }),
+ },
+ templateEdit: {
+ text: i18n.translate('xpack.idxMgmt.breadcrumb.editTemplateLabel', {
+ defaultMessage: 'Edit template',
+ }),
+ },
+ templateClone: {
+ text: i18n.translate('xpack.idxMgmt.breadcrumb.cloneTemplateLabel', {
+ defaultMessage: 'Clone template',
+ }),
+ },
+};
+
+export const setBreadcrumbs = (type?: 'templateCreate' | 'templateEdit' | 'templateClone') => {
+ const breadcrumbs = type
+ ? [MANAGEMENT_BREADCRUMB, homeBreadcrumb, templatesBreadcrumb, breadcrumbsMap[type]]
+ : [MANAGEMENT_BREADCRUMB, homeBreadcrumb];
+ chrome.breadcrumbs.set(breadcrumbs);
+};
diff --git a/x-pack/legacy/plugins/index_management/public/services/template_validation/index.ts b/x-pack/legacy/plugins/index_management/public/services/template_validation/index.ts
new file mode 100644
index 0000000000000..18a5ce667987d
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/services/template_validation/index.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { removeEmptyErrorFields, isValid, isStringEmpty } from './validation_helpers';
+export { validateLogistics } from './validation_logistics';
+export { validateSettings } from './validation_settings';
+export { validateMappings } from './validation_mappings';
+export { validateAliases } from './validation_aliases';
+export { TemplateValidation } from './types';
diff --git a/x-pack/legacy/plugins/index_management/public/services/template_validation/types.ts b/x-pack/legacy/plugins/index_management/public/services/template_validation/types.ts
new file mode 100644
index 0000000000000..c201195f7e658
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/services/template_validation/types.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface TemplateValidation {
+ isValid: boolean;
+ errors: { [key: string]: React.ReactNode[] };
+}
diff --git a/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_aliases.ts b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_aliases.ts
new file mode 100644
index 0000000000000..f92c39cba065d
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_aliases.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Template } from '../../../common/types';
+import { TemplateValidation } from './types';
+import { isStringEmpty, removeEmptyErrorFields, isValid, validateJSON } from './validation_helpers';
+
+export const validateAliases = (template: Template): TemplateValidation => {
+ const { aliases } = template;
+
+ const validation: TemplateValidation = {
+ isValid: true,
+ errors: {
+ aliases: [],
+ },
+ };
+
+ // Aliases JSON validation
+ if (typeof aliases === 'string' && !isStringEmpty(aliases)) {
+ const validationMsg = validateJSON(aliases);
+
+ if (typeof validationMsg === 'string') {
+ validation.errors.aliases.push(validationMsg);
+ }
+ }
+
+ validation.errors = removeEmptyErrorFields(validation.errors);
+ validation.isValid = isValid(validation.errors);
+
+ return validation;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_helpers.ts b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_helpers.ts
new file mode 100644
index 0000000000000..4755a110a95d3
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_helpers.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { TemplateValidation } from './types';
+
+export const removeEmptyErrorFields = (errors: TemplateValidation['errors']) => {
+ return Object.entries(errors)
+ .filter(([_key, value]) => value.length > 0)
+ .reduce((errs: TemplateValidation['errors'], [key, value]) => {
+ errs[key] = value;
+ return errs;
+ }, {});
+};
+
+export const isValid = (errors: TemplateValidation['errors']) => {
+ return Boolean(Object.keys(errors).length === 0);
+};
+
+export const isStringEmpty = (str: string | null): boolean => {
+ return str ? !Boolean(str.trim()) : true;
+};
+
+export const validateJSON = (jsonString: string) => {
+ const invalidJsonMsg = i18n.translate('xpack.idxMgmt.templateValidation.invalidJSONError', {
+ defaultMessage: 'Invalid JSON format.',
+ });
+
+ try {
+ const parsedSettingsJson = JSON.parse(jsonString);
+ if (parsedSettingsJson && typeof parsedSettingsJson !== 'object') {
+ return invalidJsonMsg;
+ }
+ return;
+ } catch (e) {
+ return invalidJsonMsg;
+ }
+};
diff --git a/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_logistics.ts b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_logistics.ts
new file mode 100644
index 0000000000000..c66a11306a006
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_logistics.ts
@@ -0,0 +1,127 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { i18n } from '@kbn/i18n';
+import {
+ ILLEGAL_CHARACTERS,
+ CONTAINS_SPACES,
+ validateIndexPattern as getIndexPatternErrors,
+} from 'ui/index_patterns';
+import { Template } from '../../../common/types';
+import { TemplateValidation } from './types';
+import { isStringEmpty, removeEmptyErrorFields, isValid } from './validation_helpers';
+
+const validateIndexPattern = (indexPattern: string) => {
+ if (indexPattern) {
+ const errors = getIndexPatternErrors(indexPattern);
+
+ if (errors[ILLEGAL_CHARACTERS]) {
+ return i18n.translate('xpack.idxMgmt.templateValidation.indexPatternInvalidCharactersError', {
+ defaultMessage:
+ "'{indexPattern}' index pattern contains the invalid {characterListLength, plural, one {character} other {characters}} { characterList }.",
+ values: {
+ characterList: errors[ILLEGAL_CHARACTERS].join(' '),
+ characterListLength: errors[ILLEGAL_CHARACTERS].length,
+ indexPattern,
+ },
+ });
+ }
+
+ if (errors[CONTAINS_SPACES]) {
+ return i18n.translate('xpack.idxMgmt.templateValidation.indexPatternSpacesError', {
+ defaultMessage: "'{indexPattern}' index pattern contains spaces.",
+ values: {
+ indexPattern,
+ },
+ });
+ }
+ }
+};
+
+export const INVALID_NAME_CHARS = ['"', '*', '\\', ',', '?'];
+
+const doesStringIncludeChar = (string: string, chars: string[]) => {
+ const invalidChar = chars.find(char => string.includes(char)) || null;
+ const containsChar = invalidChar !== null;
+
+ return { containsChar, invalidChar };
+};
+
+export const validateLogistics = (template: Template): TemplateValidation => {
+ const { name, indexPatterns } = template;
+
+ const validation: TemplateValidation = {
+ isValid: true,
+ errors: {
+ indexPatterns: [],
+ name: [],
+ },
+ };
+
+ // Name validation
+ if (name !== undefined && isStringEmpty(name)) {
+ validation.errors.name.push(
+ i18n.translate('xpack.idxMgmt.templateValidation.templateNameRequiredError', {
+ defaultMessage: 'A template name is required.',
+ })
+ );
+ } else {
+ if (name.includes(' ')) {
+ validation.errors.name.push(
+ i18n.translate('xpack.idxMgmt.templateValidation.templateNameSpacesError', {
+ defaultMessage: 'Spaces are not allowed in a template name.',
+ })
+ );
+ }
+
+ if (name.startsWith('_')) {
+ validation.errors.name.push(
+ i18n.translate('xpack.idxMgmt.templateValidation.templateNameUnderscoreError', {
+ defaultMessage: 'A template name must not start with an underscore.',
+ })
+ );
+ }
+
+ if (name.startsWith('.')) {
+ validation.errors.name.push(
+ i18n.translate('xpack.idxMgmt.templateValidation.templateNamePeriodError', {
+ defaultMessage: 'A template name must not start with a period.',
+ })
+ );
+ }
+
+ const { containsChar, invalidChar } = doesStringIncludeChar(name, INVALID_NAME_CHARS);
+
+ if (containsChar) {
+ validation.errors.name = [
+ i18n.translate('xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError', {
+ defaultMessage: 'A template name must not contain the character "{invalidChar}"',
+ values: { invalidChar },
+ }),
+ ];
+ }
+ }
+
+ // Index patterns validation
+ if (Array.isArray(indexPatterns) && indexPatterns.length === 0) {
+ validation.errors.indexPatterns.push(
+ i18n.translate('xpack.idxMgmt.templateValidation.indexPatternsRequiredError', {
+ defaultMessage: 'At least one index pattern is required.',
+ })
+ );
+ } else if (Array.isArray(indexPatterns) && indexPatterns.length) {
+ indexPatterns.forEach(pattern => {
+ const errorMsg = validateIndexPattern(pattern);
+ if (errorMsg) {
+ validation.errors.indexPatterns.push(errorMsg);
+ }
+ });
+ }
+
+ validation.errors = removeEmptyErrorFields(validation.errors);
+ validation.isValid = isValid(validation.errors);
+
+ return validation;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_mappings.ts b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_mappings.ts
new file mode 100644
index 0000000000000..fccdf09a9f32f
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_mappings.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Template } from '../../../common/types';
+import { TemplateValidation } from './types';
+import { isStringEmpty, removeEmptyErrorFields, isValid, validateJSON } from './validation_helpers';
+
+export const validateMappings = (template: Template): TemplateValidation => {
+ const { mappings } = template;
+
+ const validation: TemplateValidation = {
+ isValid: true,
+ errors: {
+ mappings: [],
+ },
+ };
+
+ // Mappings JSON validation
+ if (typeof mappings === 'string' && !isStringEmpty(mappings)) {
+ const validationMsg = validateJSON(mappings);
+
+ if (typeof validationMsg === 'string') {
+ validation.errors.mappings.push(validationMsg);
+ }
+ }
+
+ validation.errors = removeEmptyErrorFields(validation.errors);
+ validation.isValid = isValid(validation.errors);
+
+ return validation;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_settings.ts b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_settings.ts
new file mode 100644
index 0000000000000..c909e77217b66
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/services/template_validation/validation_settings.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Template } from '../../../common/types';
+import { TemplateValidation } from './types';
+import { isStringEmpty, removeEmptyErrorFields, isValid, validateJSON } from './validation_helpers';
+
+export const validateSettings = (template: Template): TemplateValidation => {
+ const { settings } = template;
+
+ const validation: TemplateValidation = {
+ isValid: true,
+ errors: {
+ settings: [],
+ },
+ };
+
+ // Settings JSON validation
+ if (typeof settings === 'string' && !isStringEmpty(settings)) {
+ const validationMsg = validateJSON(settings);
+
+ if (typeof validationMsg === 'string') {
+ validation.errors.settings.push(validationMsg);
+ }
+ }
+
+ validation.errors = removeEmptyErrorFields(validation.errors);
+ validation.isValid = isValid(validation.errors);
+
+ return validation;
+};
diff --git a/x-pack/legacy/plugins/index_management/public/services/use_request.ts b/x-pack/legacy/plugins/index_management/public/services/use_request.ts
index 2168ec4f655df..cb50ea3e9f136 100644
--- a/x-pack/legacy/plugins/index_management/public/services/use_request.ts
+++ b/x-pack/legacy/plugins/index_management/public/services/use_request.ts
@@ -4,128 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useEffect, useState } from 'react';
+import {
+ SendRequestConfig,
+ SendRequestResponse,
+ UseRequestConfig,
+ sendRequest as _sendRequest,
+ useRequest as _useRequest,
+} from '../shared_imports';
import { getHttpClient } from './api';
-import { trackUiMetric, METRIC_TYPE } from './track_ui_metric';
-interface SendRequest {
- path?: string;
- method: string;
- body?: any;
- uimActionType?: string;
-}
-
-interface SendRequestResponse {
- data: any;
- error: Error;
-}
-
-export const sendRequest = async ({
- path,
- method,
- body,
- uimActionType,
-}: SendRequest): Promise> => {
- try {
- const response = await (getHttpClient() as any)[method](path, body);
-
- if (typeof response.data === 'undefined') {
- throw new Error(response.statusText);
- }
-
- // Track successful request
- if (uimActionType) {
- trackUiMetric(METRIC_TYPE.COUNT, uimActionType);
- }
-
- return {
- data: response.data,
- };
- } catch (e) {
- return {
- error: e.response ? e.response : e,
- };
- }
+export const sendRequest = (config: SendRequestConfig): Promise> => {
+ return _sendRequest(getHttpClient(), config);
};
-interface UseRequest extends SendRequest {
- interval?: number;
- initialData?: any;
- processData?: any;
-}
-
-export const useRequest = ({
- path,
- method,
- body,
- interval,
- initialData,
- processData,
-}: UseRequest) => {
- const [error, setError] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [data, setData] = useState(initialData);
-
- // Tied to every render and bound to each request.
- let isOutdatedRequest = false;
-
- const createRequest = async (isInitialRequest = true) => {
- // Set a neutral state for a non-request.
- if (!path) {
- setError(null);
- setData(initialData);
- setIsLoading(false);
- return;
- }
-
- setError(null);
-
- // Only set loading state to true and initial data on the first request
- if (isInitialRequest) {
- setIsLoading(true);
- setData(initialData);
- }
-
- const { data: responseData, error: responseError } = await sendRequest({
- path,
- method,
- body,
- });
-
- // Don't update state if an outdated request has resolved.
- if (isOutdatedRequest) {
- return;
- }
-
- setError(responseError);
- setData(processData && responseData ? processData(responseData) : responseData);
- setIsLoading(false);
- };
-
- useEffect(() => {
- function cancelOutdatedRequest() {
- isOutdatedRequest = true;
- }
-
- createRequest();
-
- if (interval) {
- const intervalRequest = setInterval(createRequest.bind(null, false), interval);
-
- return () => {
- cancelOutdatedRequest();
- clearInterval(intervalRequest);
- };
- }
-
- // Called when a new render will trigger this effect.
- return cancelOutdatedRequest;
- }, [path]);
-
- return {
- error,
- isLoading,
- data,
- createRequest,
- };
+export const useRequest = (config: UseRequestConfig) => {
+ return _useRequest(getHttpClient(), config);
};
diff --git a/x-pack/legacy/plugins/index_management/public/shared_imports.ts b/x-pack/legacy/plugins/index_management/public/shared_imports.ts
new file mode 100644
index 0000000000000..3d93b882733ab
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/public/shared_imports.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export {
+ SendRequestConfig,
+ SendRequestResponse,
+ UseRequestConfig,
+ sendRequest,
+ useRequest,
+} from '../../../../../src/plugins/es_ui_shared/public/request';
diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/index.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/index.ts
index dc9a0acaaf09b..2d0b031dba910 100644
--- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/index.ts
+++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { registerTemplatesRoutes } from './register_templates_routes';
+export { registerTemplateRoutes } from './register_template_routes';
diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_create_route.ts
index 39092ab3ad75b..e134a97dd029e 100644
--- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_create_route.ts
+++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_create_route.ts
@@ -10,18 +10,14 @@ import {
RouterRouteHandler,
wrapCustomError,
} from '../../../../../../server/lib/create_router';
-import { Template } from '../../../../common/types';
+import { Template, TemplateEs } from '../../../../common/types';
+import { serializeTemplate } from '../../../../common/lib';
const handler: RouterRouteHandler = async (req, callWithRequest) => {
- const {
- name = '',
- order,
- version,
- settings = {},
- mappings = {},
- aliases = {},
- indexPatterns = [],
- } = req.payload as Template;
+ const template = req.payload as Template;
+ const serializedTemplate = serializeTemplate(template) as TemplateEs;
+
+ const { name, order, index_patterns, version, settings, mappings, aliases } = serializedTemplate;
const conflictError = wrapCustomError(
new Error(
@@ -54,7 +50,7 @@ const handler: RouterRouteHandler = async (req, callWithRequest) => {
name,
order,
body: {
- index_patterns: indexPatterns,
+ index_patterns,
version,
settings,
mappings,
diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts
index 7ed94ba73b13c..178505ab4c1ba 100644
--- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts
+++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts
@@ -4,33 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { deserializeTemplate, deserializeTemplateList } from '../../../../common/lib';
import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
const allHandler: RouterRouteHandler = async (_req, callWithRequest) => {
const indexTemplatesByName = await callWithRequest('indices.getTemplate');
- const indexTemplateNames = Object.keys(indexTemplatesByName);
- const indexTemplates = indexTemplateNames.map(name => {
- const {
- version,
- order,
- index_patterns: indexPatterns = [],
- settings = {},
- aliases = {},
- mappings = {},
- } = indexTemplatesByName[name];
- return {
- name,
- version,
- order,
- indexPatterns: indexPatterns.sort(),
- settings,
- aliases,
- mappings,
- };
- });
-
- return indexTemplates;
+ return deserializeTemplateList(indexTemplatesByName);
};
const oneHandler: RouterRouteHandler = async (req, callWithRequest) => {
@@ -38,24 +18,7 @@ const oneHandler: RouterRouteHandler = async (req, callWithRequest) => {
const indexTemplateByName = await callWithRequest('indices.getTemplate', { name });
if (indexTemplateByName[name]) {
- const {
- version,
- order,
- index_patterns: indexPatterns = [],
- settings = {},
- aliases = {},
- mappings = {},
- } = indexTemplateByName[name];
-
- return {
- name,
- version,
- order,
- indexPatterns: indexPatterns.sort(),
- settings,
- aliases,
- mappings,
- };
+ return deserializeTemplate({ ...indexTemplateByName[name], name });
}
};
diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_template_routes.ts
similarity index 81%
rename from x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts
rename to x-pack/legacy/plugins/index_management/server/routes/api/templates/register_template_routes.ts
index 084abd0a91eb3..183930315bcdd 100644
--- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts
+++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_template_routes.ts
@@ -8,10 +8,12 @@ import { Router } from '../../../../../../server/lib/create_router';
import { registerGetAllRoute, registerGetOneRoute } from './register_get_routes';
import { registerDeleteRoute } from './register_delete_route';
import { registerCreateRoute } from './register_create_route';
+import { registerUpdateRoute } from './register_update_route';
-export function registerTemplatesRoutes(router: Router) {
+export function registerTemplateRoutes(router: Router) {
registerGetAllRoute(router);
registerGetOneRoute(router);
registerDeleteRoute(router);
registerCreateRoute(router);
+ registerUpdateRoute(router);
}
diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_update_route.ts
new file mode 100644
index 0000000000000..15590e2acbe71
--- /dev/null
+++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_update_route.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router';
+import { Template, TemplateEs } from '../../../../common/types';
+import { serializeTemplate } from '../../../../common/lib';
+
+const handler: RouterRouteHandler = async (req, callWithRequest) => {
+ const { name } = req.params;
+ const template = req.payload as Template;
+ const serializedTemplate = serializeTemplate(template) as TemplateEs;
+
+ const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate;
+
+ // Verify the template exists (ES will throw 404 if not)
+ await callWithRequest('indices.existsTemplate', { name });
+
+ // Next, update index template
+ return await callWithRequest('indices.putTemplate', {
+ name,
+ order,
+ body: {
+ index_patterns,
+ version,
+ settings,
+ mappings,
+ aliases,
+ },
+ });
+};
+
+export function registerUpdateRoute(router: Router) {
+ router.put('templates/{name}', handler);
+}
diff --git a/x-pack/legacy/plugins/index_management/test/fixtures/template.ts b/x-pack/legacy/plugins/index_management/test/fixtures/template.ts
index 6365705944e49..089e3534bd429 100644
--- a/x-pack/legacy/plugins/index_management/test/fixtures/template.ts
+++ b/x-pack/legacy/plugins/index_management/test/fixtures/template.ts
@@ -12,9 +12,9 @@ export const getTemplate = ({
version = getRandomNumber(),
order = getRandomNumber(),
indexPatterns = [],
- settings = {},
- aliases = {},
- mappings = {},
+ settings,
+ aliases,
+ mappings,
}: Partial = {}): Template => ({
name,
version,
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 6a82cc86a55c9..45a54ea167913 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -4690,7 +4690,6 @@
"xpack.grokDebugger.unavailableLicenseInformationMessage": "現在ライセンス情報が利用できないため {grokLogParsingTool} デバッガーを使用できません。",
"xpack.idxMgmt.appTitle": "インデックス管理",
"xpack.idxMgmt.badgeAriaLabel": "{label}。これをフィルタリングするよう選択。",
- "xpack.idxMgmt.breadcrumb": "インデックス管理",
"xpack.idxMgmt.clearCacheIndicesAction.successMessage": "キャッシュ [{indexNames}] が削除されました:",
"xpack.idxMgmt.closeIndicesAction.successfullyClosedIndicesMessage": "[{indexNames}] がクローズされました",
"xpack.idxMgmt.deleteIndicesAction.successfullyDeletedIndicesMessage": "[{indexNames}] が削除されました",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index b1399c686d62e..06b6f7fc6b597 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -4833,7 +4833,6 @@
"xpack.grokDebugger.unavailableLicenseInformationMessage": "您不能使用 {grokLogParsingTool} Debugger,因为许可证信息当前不可用。",
"xpack.idxMgmt.appTitle": "索引管理",
"xpack.idxMgmt.badgeAriaLabel": "{label}。选择以基于此选项进行筛选。",
- "xpack.idxMgmt.breadcrumb": "索引管理",
"xpack.idxMgmt.clearCacheIndicesAction.successMessage": "已成功清除缓存:[{indexNames}]",
"xpack.idxMgmt.closeIndicesAction.successfullyClosedIndicesMessage": "已成功关闭:[{indexNames}]",
"xpack.idxMgmt.deleteIndicesAction.successfullyDeletedIndicesMessage": "已成功删除:[{indexNames}]",
diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js b/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js
index e5a696aa39ac9..055f959cc4b48 100644
--- a/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js
+++ b/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js
@@ -16,10 +16,15 @@ export const registerHelpers = ({ supertest }) => {
order: 1,
indexPatterns: INDEX_PATTERNS,
version: 1,
- settings: {
- number_of_shards: 1
- },
- mappings: {
+ settings: JSON.stringify({
+ number_of_shards: 1,
+ index: {
+ lifecycle: {
+ name: 'my_policy',
+ }
+ }
+ }),
+ mappings: JSON.stringify({
_source: {
enabled: false
},
@@ -32,10 +37,10 @@ export const registerHelpers = ({ supertest }) => {
format: 'EEE MMM dd HH:mm:ss Z yyyy'
}
}
- },
- aliases: {
+ }),
+ aliases: JSON.stringify({
alias1: {}
- }
+ })
});
const createTemplate = payload =>
@@ -49,11 +54,18 @@ export const registerHelpers = ({ supertest }) => {
.delete(`${API_BASE_PATH}/templates/${templatesToDelete.map(template => encodeURIComponent(template)).join(',')}`)
.set('kbn-xsrf', 'xxx');
+ const updateTemplate = (payload, templateName) =>
+ supertest
+ .put(`${API_BASE_PATH}/templates/${templateName}`)
+ .set('kbn-xsrf', 'xxx')
+ .send(payload);
+
return {
getAllTemplates,
getOneTemplate,
getTemplatePayload,
createTemplate,
+ updateTemplate,
deleteTemplates,
};
};
diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js
index 517966d342232..a6a7493218499 100644
--- a/x-pack/test/api_integration/apis/management/index_management/templates.js
+++ b/x-pack/test/api_integration/apis/management/index_management/templates.js
@@ -24,17 +24,26 @@ export default function ({ getService }) {
createTemplate,
getTemplatePayload,
deleteTemplates,
+ updateTemplate,
} = registerHelpers({ supertest });
describe('index templates', () => {
after(() => Promise.all([cleanUpEsResources()]));
describe('get all', () => {
+ const templateName = `template-${getRandomString()}`;
+ const payload = getTemplatePayload(templateName);
+
+ beforeEach(async () => {
+ await createTemplate(payload).expect(200);
+ });
+
it('should list all the index templates with the expected properties', async () => {
- const { body } = await getAllTemplates().expect(200);
- const expectedKeys = ['name', 'indexPatterns', 'settings', 'aliases', 'mappings'];
+ const { body: templates } = await getAllTemplates().expect(200);
- expectedKeys.forEach(key => expect(Object.keys(body[0]).includes(key)).to.be(true));
+ const createdTemplate = templates.find(template => template.name === payload.name);
+ const expectedKeys = ['name', 'indexPatterns', 'hasSettings', 'hasAliases', 'hasMappings', 'ilmPolicy'];
+ expectedKeys.forEach(key => expect(Object.keys(createdTemplate).includes(key)).to.be(true));
});
});
@@ -48,7 +57,7 @@ export default function ({ getService }) {
it('should list the index template with the expected properties', async () => {
const { body } = await getOneTemplate(templateName).expect(200);
- const expectedKeys = ['name', 'indexPatterns', 'settings', 'aliases', 'mappings'];
+ const expectedKeys = ['name', 'indexPatterns', 'settings', 'aliases', 'mappings', 'ilmPolicy'];
expect(body.name).to.equal(templateName);
expectedKeys.forEach(key => expect(Object.keys(body).includes(key)).to.be(true));
@@ -83,6 +92,29 @@ export default function ({ getService }) {
});
});
+ describe('update', () => {
+ it('should update an index template', async () => {
+ const templateName = `template-${getRandomString()}`;
+ const payload = getTemplatePayload(templateName);
+
+ await createTemplate(payload).expect(200);
+
+ let catTemplateResponse = await catTemplate(templateName);
+
+ const { name, version } = payload;
+
+ expect(catTemplateResponse.find(({ name: templateName }) => templateName === name).version).to.equal(version.toString());
+
+ // Update template with new version
+ const updatedVersion = 2;
+ await updateTemplate({ ...payload, version: updatedVersion }, templateName).expect(200);
+
+ catTemplateResponse = await catTemplate(templateName);
+
+ expect(catTemplateResponse.find(({ name: templateName }) => templateName === name).version).to.equal(updatedVersion.toString());
+ });
+ });
+
describe('delete', () => {
it('should delete an index template', async () => {
const templateName = `template-${getRandomString()}`;
diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts
index 687b508df6105..c6b7517fc1858 100644
--- a/x-pack/test/functional/apps/index_management/home_page.ts
+++ b/x-pack/test/functional/apps/index_management/home_page.ts
@@ -8,8 +8,10 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
- const pageObjects = getPageObjects(['common', 'indexManagement']);
+ const testSubjects = getService('testSubjects');
+ const pageObjects = getPageObjects(['common', 'indexManagement', 'header']);
const log = getService('log');
+ const browser = getService('browser');
describe('Home page', function() {
this.tags('smoke');
@@ -19,11 +21,32 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('Loads the app', async () => {
await log.debug('Checking for section heading to say Index Management.');
+
const headingText = await pageObjects.indexManagement.sectionHeadingText();
expect(headingText).to.be('Index Management');
+ const indicesList = await testSubjects.exists('indicesList');
+ expect(indicesList).to.be(true);
+
const reloadIndicesButton = await pageObjects.indexManagement.reloadIndicesButton();
expect(await reloadIndicesButton.isDisplayed()).to.be(true);
});
+
+ describe('Index templates', () => {
+ it('renders the index templates tab', async () => {
+ // Navigate to the index templates tab
+ pageObjects.indexManagement.changeTabs('templatesTab');
+
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ // Verify url
+ const url = await browser.getCurrentUrl();
+ expect(url).to.contain(`/templates`);
+
+ // Verify content
+ const templateList = await testSubjects.exists('templateList');
+ expect(templateList).to.be(true);
+ });
+ });
});
};
diff --git a/x-pack/test/functional/page_objects/index_management_page.ts b/x-pack/test/functional/page_objects/index_management_page.ts
index a8e02b6e7e805..39a672b163eb3 100644
--- a/x-pack/test/functional/page_objects/index_management_page.ts
+++ b/x-pack/test/functional/page_objects/index_management_page.ts
@@ -16,5 +16,8 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext)
async reloadIndicesButton() {
return await testSubjects.find('reloadIndicesButton');
},
+ async changeTabs(tab: 'indicesTab' | 'templatesTab') {
+ return await testSubjects.click(tab);
+ },
};
}