From 3d3767b6f0d4d9d8f7628c3f988c37c3b7906740 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Loix?=
Date: Fri, 3 Apr 2020 17:41:09 +0200
Subject: [PATCH] =?UTF-8?q?[7.x]=20[Index=20management]=20Prepare=20suppor?=
=?UTF-8?q?t=20Index=20template=20V2=20for=E2=80=A6=20(#62400)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../public/request/np_ready_request.ts | 2 +-
.../components/form_data_provider.ts | 5 +-
.../helpers/home.helpers.ts | 13 +-
.../helpers/template_form.helpers.ts | 36 +++--
.../__jest__/client_integration/home.test.ts | 56 ++++----
.../template_clone.test.tsx | 12 +-
.../template_create.test.tsx | 46 ++++---
.../client_integration/template_edit.test.tsx | 83 +++++++-----
.../common/constants/index.ts | 1 +
.../common/constants/index_templates.ts | 12 ++
.../plugins/index_management/common/index.ts | 6 +-
.../index_management/common/lib/index.ts | 6 +-
.../common/lib/template_serialization.ts | 128 +++++++++++-------
.../index_management/common/lib/utils.test.ts | 35 +++++
.../index_management/common/lib/utils.ts | 29 ++++
.../index_management/common/types/aliases.ts | 9 ++
.../index_management/common/types/index.ts | 6 +
.../index_management/common/types/indices.ts | 43 ++++++
.../index_management/common/types/mappings.ts | 11 ++
.../common/types/templates.ts | 79 +++++++----
.../load_mappings_provider.test.tsx | 14 +-
.../mappings_editor/mappings_state.tsx | 3 +-
.../components/mappings_editor/reducer.ts | 16 ++-
.../components/template_delete_modal.tsx | 21 ++-
.../template_form/steps/step_aliases.tsx | 2 +-
.../template_form/steps/step_mappings.tsx | 19 ++-
.../template_form/steps/step_review.tsx | 34 +++--
.../template_form/steps/step_settings.tsx | 2 +-
.../template_form/steps/use_json_step.ts | 10 +-
.../template_form/template_form.tsx | 51 +++++--
.../components/template_form/types.ts | 13 +-
.../public/application/lib/index_templates.ts | 17 +++
.../template_details/tabs/tab_aliases.tsx | 8 +-
.../template_details/tabs/tab_mappings.tsx | 8 +-
.../template_details/tabs/tab_settings.tsx | 8 +-
.../template_details/tabs/tab_summary.tsx | 11 +-
.../template_details/template_details.tsx | 31 +++--
.../home/template_list/template_list.tsx | 30 ++--
.../template_table/template_table.tsx | 39 +++---
.../template_clone/template_clone.tsx | 18 ++-
.../template_create/template_create.tsx | 7 +-
.../sections/template_edit/template_edit.tsx | 17 ++-
.../public/application/services/api.ts | 37 +++--
.../public/application/services/routing.ts | 23 +++-
.../application/services/use_request.ts | 4 +-
.../api/templates/register_create_route.ts | 18 ++-
.../api/templates/register_delete_route.ts | 32 +++--
.../api/templates/register_get_routes.ts | 25 +++-
.../api/templates/register_update_route.ts | 16 ++-
.../routes/api/templates/validate_schemas.ts | 13 +-
.../test/fixtures/template.ts | 24 ++--
.../index_management/templates.helpers.js | 57 ++++----
.../management/index_management/templates.js | 31 +++--
x-pack/test_utils/testbed/testbed.ts | 39 ++++++
x-pack/test_utils/testbed/types.ts | 6 +
55 files changed, 917 insertions(+), 405 deletions(-)
create mode 100644 x-pack/plugins/index_management/common/constants/index_templates.ts
create mode 100644 x-pack/plugins/index_management/common/lib/utils.test.ts
create mode 100644 x-pack/plugins/index_management/common/lib/utils.ts
create mode 100644 x-pack/plugins/index_management/common/types/aliases.ts
create mode 100644 x-pack/plugins/index_management/common/types/indices.ts
create mode 100644 x-pack/plugins/index_management/common/types/mappings.ts
create mode 100644 x-pack/plugins/index_management/public/application/lib/index_templates.ts
diff --git a/src/plugins/es_ui_shared/public/request/np_ready_request.ts b/src/plugins/es_ui_shared/public/request/np_ready_request.ts
index 6771abd64df7e0..06af698f2ce023 100644
--- a/src/plugins/es_ui_shared/public/request/np_ready_request.ts
+++ b/src/plugins/es_ui_shared/public/request/np_ready_request.ts
@@ -43,7 +43,7 @@ export interface UseRequestResponse {
isInitialRequest: boolean;
isLoading: boolean;
error: E | null;
- data: D | null;
+ data?: D | null;
sendRequest: (...args: any[]) => Promise>;
}
diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
index a8d24984cec7cf..0509b8081c35be 100644
--- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
+++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts
@@ -28,9 +28,9 @@ interface Props {
}
export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => {
- const [formData, setFormData] = useState({});
- const previousRawData = useRef({});
const form = useFormContext();
+ const previousRawData = useRef(form.__formData$.current.value);
+ const [formData, setFormData] = useState(previousRawData.current);
useEffect(() => {
const subscription = form.subscribe(({ data: { raw } }) => {
@@ -41,6 +41,7 @@ export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) =
const valuesToWatchArray = Array.isArray(pathsToWatch)
? (pathsToWatch as string[])
: ([pathsToWatch] as string[]);
+
if (valuesToWatchArray.some(value => previousRawData.current[value] !== raw[value])) {
previousRawData.current = raw;
setFormData(raw);
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts
index 7e3e1fba9c44a6..397a78354f4707 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts
@@ -16,7 +16,7 @@ import {
import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths
import { BASE_PATH } from '../../../common/constants';
import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths
-import { Template } from '../../../common/types';
+import { TemplateDeserialized } from '../../../common';
import { WithAppDependencies, services } from './setup_environment';
const testBedConfig: TestBedConfig = {
@@ -36,10 +36,13 @@ export interface IdxMgmtHomeTestBed extends TestBed {
selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void;
selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void;
clickReloadButton: () => void;
- clickTemplateAction: (name: Template['name'], action: 'edit' | 'clone' | 'delete') => void;
+ clickTemplateAction: (
+ name: TemplateDeserialized['name'],
+ action: 'edit' | 'clone' | 'delete'
+ ) => void;
clickTemplateAt: (index: number) => void;
clickCloseDetailsButton: () => void;
- clickActionMenu: (name: Template['name']) => void;
+ clickActionMenu: (name: TemplateDeserialized['name']) => void;
};
}
@@ -78,7 +81,7 @@ export const setup = async (): Promise => {
find('reloadButton').simulate('click');
};
- const clickActionMenu = async (templateName: Template['name']) => {
+ const clickActionMenu = async (templateName: TemplateDeserialized['name']) => {
const { component } = testBed;
// When a table has > 2 actions, EUI displays an overflow menu with an id "-actions"
@@ -87,7 +90,7 @@ export const setup = async (): Promise => {
};
const clickTemplateAction = (
- templateName: Template['name'],
+ templateName: TemplateDeserialized['name'],
action: 'edit' | 'clone' | 'delete'
) => {
const actions = ['edit', 'clone', 'delete'];
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
index c8b1322b6100e9..e565432e07fe1e 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts
@@ -5,7 +5,7 @@
*/
import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils';
-import { Template } from '../../../common/types';
+import { TemplateDeserialized } from '../../../common';
import { nextTick } from './index';
interface MappingField {
@@ -63,8 +63,8 @@ export const formSetup = async (initTestBed: SetupFunc) => {
indexPatterns,
order,
version,
- }: Partial = {}) => {
- const { form, find, component } = testBed;
+ }: Partial = {}) => {
+ const { form, find, waitFor } = testBed;
if (name) {
form.setInputValue('nameField.input', name);
@@ -89,12 +89,11 @@ export const formSetup = async (initTestBed: SetupFunc) => {
}
clickNextButton();
- await nextTick();
- component.update();
+ await waitFor('stepSettings');
};
const completeStepTwo = async (settings?: string) => {
- const { find, component } = testBed;
+ const { find, component, waitFor } = testBed;
if (settings) {
find('mockCodeEditor').simulate('change', {
@@ -105,42 +104,41 @@ export const formSetup = async (initTestBed: SetupFunc) => {
}
clickNextButton();
- await nextTick();
- component.update();
+ await waitFor('stepMappings');
};
const completeStepThree = async (mappingFields?: MappingField[]) => {
- const { component } = testBed;
+ const { waitFor } = testBed;
if (mappingFields) {
for (const field of mappingFields) {
const { name, type } = field;
await addMappingField(name, type);
}
- } else {
- await nextTick();
}
- await nextTick(50); // hooks updates cycles are tricky, adding some latency is needed
clickNextButton();
- await nextTick(50);
- component.update();
+ await waitFor('stepAliases');
};
- const completeStepFour = async (aliases?: string) => {
- const { find, component } = testBed;
+ const completeStepFour = async (aliases?: string, waitForNextStep = true) => {
+ const { find, component, waitFor } = testBed;
if (aliases) {
find('mockCodeEditor').simulate('change', {
jsonString: aliases,
}); // Using mocked EuiCodeEditor
- await nextTick(50);
+ await nextTick();
component.update();
}
clickNextButton();
- await nextTick(50);
- component.update();
+
+ if (waitForNextStep) {
+ await waitFor('summaryTab');
+ } else {
+ component.update();
+ }
};
const selectSummaryTab = (tab: 'summary' | 'request') => {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts
index 9e8af02b74631c..a987535e0c291f 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts
@@ -115,11 +115,13 @@ describe('', () => {
const template1 = fixtures.getTemplate({
name: `a${getRandomString()}`,
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
- settings: {
- index: {
- number_of_shards: '1',
- lifecycle: {
- name: 'my_ilm_policy',
+ template: {
+ settings: {
+ index: {
+ number_of_shards: '1',
+ lifecycle: {
+ name: 'my_ilm_policy',
+ },
},
},
},
@@ -302,7 +304,10 @@ describe('', () => {
const templateId = rows[0].columns[2].value;
- const { name: templateName } = template1;
+ const {
+ name: templateName,
+ _kbnMeta: { formatVersion },
+ } = template1;
await actions.clickTemplateAction(templateName, 'delete');
const modal = document.body.querySelector(
@@ -327,8 +332,11 @@ describe('', () => {
const latestRequest = server.requests[server.requests.length - 1];
- expect(latestRequest.method).toBe('DELETE');
- expect(latestRequest.url).toBe(`${API_BASE_PATH}/templates/${template1.name}`);
+ expect(latestRequest.method).toBe('POST');
+ expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-templates`);
+ expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({
+ templates: [{ name: template1.name, formatVersion }],
+ });
});
});
@@ -396,24 +404,26 @@ describe('', () => {
const template = fixtures.getTemplate({
name: `a${getRandomString()}`,
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
- settings: {
- index: {
- number_of_shards: '1',
- },
- },
- mappings: {
- _source: {
- enabled: false,
+ template: {
+ settings: {
+ index: {
+ number_of_shards: '1',
+ },
},
- properties: {
- created_at: {
- type: 'date',
- format: 'EEE MMM dd HH:mm:ss Z yyyy',
+ mappings: {
+ _source: {
+ enabled: false,
+ },
+ properties: {
+ created_at: {
+ type: 'date',
+ format: 'EEE MMM dd HH:mm:ss Z yyyy',
+ },
},
},
- },
- aliases: {
- alias1: {},
+ aliases: {
+ alias1: {},
+ },
},
});
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
index 5d895c8e986242..17e19bf881dee9 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx
@@ -54,21 +54,17 @@ describe('', () => {
const templateToClone = getTemplate({
name: TEMPLATE_NAME,
indexPatterns: ['indexPattern1'],
- mappings: {
- ...MAPPINGS,
- _meta: {},
- _source: {},
+ template: {
+ mappings: MAPPINGS,
},
});
beforeEach(async () => {
httpRequestsMockHelpers.setLoadTemplateResponse(templateToClone);
- testBed = await setup();
-
await act(async () => {
- await nextTick();
- testBed.component.update();
+ testBed = await setup();
+ await testBed.waitFor('templateForm');
});
});
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx
index 981067c09f8aa8..ad8e8c22a87fa9 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx
@@ -6,6 +6,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
+import { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../common';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { TemplateFormTestBed } from './helpers/template_form.helpers';
import {
@@ -71,6 +72,7 @@ describe('', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
});
});
@@ -100,6 +102,7 @@ describe('', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
});
});
@@ -209,7 +212,7 @@ describe('', () => {
await act(async () => {
// Complete step 4 (aliases) with invalid json
- await actions.completeStepFour('{ invalidJsonString ');
+ await actions.completeStepFour('{ invalidJsonString ', false);
});
expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
@@ -221,6 +224,7 @@ describe('', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
const { actions } = testBed;
@@ -275,6 +279,7 @@ describe('', () => {
it('should render a warning message if a wildcard is used as an index pattern', async () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
const { actions } = testBed;
// Complete step 1 (logistics)
@@ -308,6 +313,7 @@ describe('', () => {
await act(async () => {
testBed = await setup();
+ await testBed.waitFor('templateForm');
const { actions } = testBed;
// Complete step 1 (logistics)
@@ -323,7 +329,6 @@ describe('', () => {
await actions.completeStepThree(MAPPING_FIELDS);
// Complete step 4 (aliases)
- await nextTick(100);
await actions.completeStepFour(JSON.stringify(ALIASES));
});
});
@@ -338,29 +343,34 @@ describe('', () => {
const latestRequest = server.requests[server.requests.length - 1];
- const expected = JSON.stringify({
+ const expected = {
isManaged: false,
name: TEMPLATE_NAME,
indexPatterns: DEFAULT_INDEX_PATTERNS,
- settings: SETTINGS,
- mappings: {
- ...MAPPINGS,
- properties: {
- [BOOLEAN_MAPPING_FIELD.name]: {
- type: BOOLEAN_MAPPING_FIELD.type,
- },
- [TEXT_MAPPING_FIELD.name]: {
- type: TEXT_MAPPING_FIELD.type,
- },
- [KEYWORD_MAPPING_FIELD.name]: {
- type: KEYWORD_MAPPING_FIELD.type,
+ template: {
+ settings: SETTINGS,
+ mappings: {
+ ...MAPPINGS,
+ properties: {
+ [BOOLEAN_MAPPING_FIELD.name]: {
+ type: BOOLEAN_MAPPING_FIELD.type,
+ },
+ [TEXT_MAPPING_FIELD.name]: {
+ type: TEXT_MAPPING_FIELD.type,
+ },
+ [KEYWORD_MAPPING_FIELD.name]: {
+ type: KEYWORD_MAPPING_FIELD.type,
+ },
},
},
+ aliases: ALIASES,
},
- aliases: ALIASES,
- });
+ _kbnMeta: {
+ formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT,
+ },
+ };
- expect(JSON.parse(latestRequest.requestBody).body).toEqual(expected);
+ expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
});
it('should surface the API errors from the put HTTP request', async () => {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
index 537b0d8ef41563..5b10ff226022d4 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx
@@ -99,17 +99,17 @@ describe('', () => {
const templateToEdit = fixtures.getTemplate({
name: TEMPLATE_NAME,
indexPatterns: ['indexPattern1'],
- mappings: MAPPING,
+ template: {
+ mappings: MAPPING,
+ },
});
beforeEach(async () => {
httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit);
- testBed = await setup();
-
await act(async () => {
- await nextTick();
- testBed.component.update();
+ testBed = await setup();
+ await testBed.waitFor('templateForm');
});
});
@@ -128,10 +128,9 @@ describe('', () => {
expect(nameInput.props().disabled).toEqual(true);
});
- // TODO: Flakey test
- describe.skip('form payload', () => {
+ describe('form payload', () => {
beforeEach(async () => {
- const { actions, component, find, form } = testBed;
+ const { actions } = testBed;
await act(async () => {
// Complete step 1 (logistics)
@@ -141,20 +140,32 @@ describe('', () => {
// Step 2 (index settings)
await actions.completeStepTwo(JSON.stringify(SETTINGS));
+ });
+ });
+
+ it('should send the correct payload with changed values', async () => {
+ const { actions, component, find, form } = testBed;
- // Step 3 (mappings)
- // Select the first field to edit
- actions.clickEditButtonAtField(0);
+ await act(async () => {
+ // Make some changes to the mappings (step 3)
+
+ actions.clickEditButtonAtField(0); // Select the first field to edit
await nextTick();
component.update();
- // verify edit field flyout
- expect(find('mappingsEditorFieldEdit').length).toEqual(1);
- // change field name
+ });
+
+ // verify edit field flyout
+ expect(find('mappingsEditorFieldEdit').length).toEqual(1);
+
+ await act(async () => {
+ // change the field name
form.setInputValue('nameParameterInput', UPDATED_MAPPING_TEXT_FIELD_NAME);
+
// Save changes
actions.clickEditFieldUpdateButton();
await nextTick();
component.update();
+
// Proceed to the next step
actions.clickNextButton();
await nextTick(50);
@@ -162,19 +173,13 @@ describe('', () => {
// Step 4 (aliases)
await actions.completeStepFour(JSON.stringify(ALIASES));
- });
- });
- it('should send the correct payload with changed values', async () => {
- const { actions } = testBed;
-
- await act(async () => {
+ // Submit the form
actions.clickSubmitButton();
await nextTick();
});
const latestRequest = server.requests[server.requests.length - 1];
-
const { version, order } = templateToEdit;
const expected = {
@@ -182,27 +187,31 @@ describe('', () => {
version,
order,
indexPatterns: UPDATED_INDEX_PATTERN,
- mappings: {
- ...MAPPING,
- _meta: {},
- _source: {},
- properties: {
- [UPDATED_MAPPING_TEXT_FIELD_NAME]: {
- type: 'text',
- store: false,
- index: true,
- fielddata: false,
- eager_global_ordinals: false,
- index_phrases: false,
- norms: true,
- index_options: 'positions',
+ template: {
+ mappings: {
+ ...MAPPING,
+ properties: {
+ [UPDATED_MAPPING_TEXT_FIELD_NAME]: {
+ type: 'text',
+ store: false,
+ index: true,
+ fielddata: false,
+ eager_global_ordinals: false,
+ index_phrases: false,
+ norms: true,
+ index_options: 'positions',
+ },
},
},
+ settings: SETTINGS,
+ aliases: ALIASES,
},
isManaged: false,
- settings: SETTINGS,
- aliases: ALIASES,
+ _kbnMeta: {
+ formatVersion: templateToEdit._kbnMeta.formatVersion,
+ },
};
+
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
});
});
diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts
index d1700f0e611c05..966e2e8e64838a 100644
--- a/x-pack/plugins/index_management/common/constants/index.ts
+++ b/x-pack/plugins/index_management/common/constants/index.ts
@@ -9,6 +9,7 @@ export { BASE_PATH } from './base_path';
export { API_BASE_PATH } from './api_base_path';
export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters';
export * from './index_statuses';
+export { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from './index_templates';
export {
UIM_APP_NAME,
diff --git a/x-pack/plugins/index_management/common/constants/index_templates.ts b/x-pack/plugins/index_management/common/constants/index_templates.ts
new file mode 100644
index 00000000000000..788e96ee895ed8
--- /dev/null
+++ b/x-pack/plugins/index_management/common/constants/index_templates.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.
+ */
+
+/**
+ * Up until the end of the 8.x release cycle we need to support both
+ * V1 and V2 index template formats. This constant keeps track of whether
+ * we create V1 or V2 index template format in the UI.
+ */
+export const DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT = 1;
diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts
index 0cc4ba79711ce9..459eda7552c852 100644
--- a/x-pack/plugins/index_management/common/index.ts
+++ b/x-pack/plugins/index_management/common/index.ts
@@ -4,4 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { PLUGIN, API_BASE_PATH } from './constants';
+export { PLUGIN, API_BASE_PATH, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from './constants';
+
+export { getTemplateParameter } from './lib';
+
+export * from './types';
diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts
index 83b22d8d72e92e..33f7fbe45182e1 100644
--- a/x-pack/plugins/index_management/common/lib/index.ts
+++ b/x-pack/plugins/index_management/common/lib/index.ts
@@ -5,6 +5,8 @@
*/
export {
deserializeTemplateList,
- deserializeTemplate,
- serializeTemplate,
+ deserializeV1Template,
+ serializeV1Template,
} from './template_serialization';
+
+export { getTemplateParameter } from './utils';
diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts
index b7d3410c775d44..33a83d1e9335b5 100644
--- a/x-pack/plugins/index_management/common/lib/template_serialization.ts
+++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts
@@ -3,46 +3,25 @@
* 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';
+import {
+ TemplateDeserialized,
+ TemplateV1Serialized,
+ TemplateV2Serialized,
+ TemplateListItem,
+} from '../types';
const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
-export function deserializeTemplateList(
- indexTemplatesByName: any,
- managedTemplatePrefix?: string
-): 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,
- isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)),
- };
- });
-
- return deserializedTemplates;
-}
-
-export function serializeTemplate(template: Template): TemplateEs {
- const { name, version, order, indexPatterns, settings, aliases, mappings } = template;
+export function serializeV1Template(template: TemplateDeserialized): TemplateV1Serialized {
+ const {
+ name,
+ version,
+ order,
+ indexPatterns,
+ template: { settings, aliases, mappings } = {} as TemplateDeserialized['template'],
+ } = template;
- const serializedTemplate: TemplateEs = {
+ const serializedTemplate: TemplateV1Serialized = {
name,
version,
order,
@@ -55,31 +34,88 @@ export function serializeTemplate(template: Template): TemplateEs {
return serializedTemplate;
}
-export function deserializeTemplate(
- templateEs: TemplateEs,
+export function serializeV2Template(template: TemplateDeserialized): TemplateV2Serialized {
+ const { aliases, mappings, settings, ...templateV1serialized } = serializeV1Template(template);
+
+ return {
+ ...templateV1serialized,
+ template: {
+ aliases,
+ mappings,
+ settings,
+ },
+ priority: template.priority,
+ composed_of: template.composedOf,
+ };
+}
+
+export function deserializeV2Template(
+ templateEs: TemplateV2Serialized,
managedTemplatePrefix?: string
-): Template {
+): TemplateDeserialized {
const {
name,
version,
order,
index_patterns: indexPatterns,
- settings,
- aliases,
- mappings,
+ template,
+ priority,
+ composed_of: composedOf,
} = templateEs;
+ const { settings } = template;
- const deserializedTemplate: Template = {
+ const deserializedTemplate: TemplateDeserialized = {
name,
version,
order,
indexPatterns: indexPatterns.sort(),
- settings,
- aliases,
- mappings,
+ template,
ilmPolicy: settings && settings.index && settings.index.lifecycle,
isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)),
+ priority,
+ composedOf,
+ _kbnMeta: {
+ formatVersion: 2,
+ },
};
return deserializedTemplate;
}
+
+export function deserializeV1Template(
+ templateEs: TemplateV1Serialized,
+ managedTemplatePrefix?: string
+): TemplateDeserialized {
+ const { settings, aliases, mappings, ...rest } = templateEs;
+
+ const deserializedTemplateV2 = deserializeV2Template(
+ { ...rest, template: { aliases, settings, mappings } },
+ managedTemplatePrefix
+ );
+
+ return {
+ ...deserializedTemplateV2,
+ _kbnMeta: {
+ formatVersion: 1,
+ },
+ };
+}
+
+export function deserializeTemplateList(
+ indexTemplatesByName: { [key: string]: Omit },
+ managedTemplatePrefix?: string
+): TemplateListItem[] {
+ return Object.entries(indexTemplatesByName).map(([name, templateSerialized]) => {
+ const {
+ template: { mappings, settings, aliases },
+ ...deserializedTemplate
+ } = deserializeV1Template({ name, ...templateSerialized }, managedTemplatePrefix);
+
+ return {
+ ...deserializedTemplate,
+ hasSettings: hasEntries(settings),
+ hasAliases: hasEntries(aliases),
+ hasMappings: hasEntries(mappings),
+ };
+ });
+}
diff --git a/x-pack/plugins/index_management/common/lib/utils.test.ts b/x-pack/plugins/index_management/common/lib/utils.test.ts
new file mode 100644
index 00000000000000..221d1b009cede0
--- /dev/null
+++ b/x-pack/plugins/index_management/common/lib/utils.test.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { TemplateV1Serialized, TemplateV2Serialized } from '../types';
+import { getTemplateVersion } from './utils';
+
+describe('utils', () => {
+ describe('getTemplateVersion', () => {
+ test('should detect v1 template', () => {
+ const template = {
+ name: 'my_template',
+ index_patterns: ['logs*'],
+ mappings: {
+ properties: {},
+ },
+ };
+ expect(getTemplateVersion(template as TemplateV1Serialized)).toBe(1);
+ });
+
+ test('should detect v2 template', () => {
+ const template = {
+ name: 'my_template',
+ index_patterns: ['logs*'],
+ template: {
+ mappings: {
+ properties: {},
+ },
+ },
+ };
+ expect(getTemplateVersion(template as TemplateV2Serialized)).toBe(2);
+ });
+ });
+});
diff --git a/x-pack/plugins/index_management/common/lib/utils.ts b/x-pack/plugins/index_management/common/lib/utils.ts
new file mode 100644
index 00000000000000..eee35dc1ab4679
--- /dev/null
+++ b/x-pack/plugins/index_management/common/lib/utils.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TemplateDeserialized, TemplateV1Serialized, TemplateV2Serialized } from '../types';
+
+/**
+ * Helper to get the format version of an index template.
+ * v1 will be supported up until 9.x but marked as deprecated from 7.8
+ * v2 will be supported from 7.8
+ */
+export const getTemplateVersion = (
+ template: TemplateDeserialized | TemplateV1Serialized | TemplateV2Serialized
+): 1 | 2 => {
+ return {}.hasOwnProperty.call(template, 'template') ? 2 : 1;
+};
+
+export const getTemplateParameter = (
+ template: TemplateV1Serialized | TemplateV2Serialized,
+ setting: 'aliases' | 'settings' | 'mappings'
+) => {
+ const formatVersion = getTemplateVersion(template);
+
+ return formatVersion === 1
+ ? (template as TemplateV1Serialized)[setting]
+ : (template as TemplateV2Serialized).template[setting];
+};
diff --git a/x-pack/plugins/index_management/common/types/aliases.ts b/x-pack/plugins/index_management/common/types/aliases.ts
new file mode 100644
index 00000000000000..76aae8585c0655
--- /dev/null
+++ b/x-pack/plugins/index_management/common/types/aliases.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface Aliases {
+ [key: string]: any;
+}
diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts
index 922cb63b70da37..b467f020978a56 100644
--- a/x-pack/plugins/index_management/common/types/index.ts
+++ b/x-pack/plugins/index_management/common/types/index.ts
@@ -4,4 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export * from './aliases';
+
+export * from './indices';
+
+export * from './mappings';
+
export * from './templates';
diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts
new file mode 100644
index 00000000000000..ecf5ba21fe60c1
--- /dev/null
+++ b/x-pack/plugins/index_management/common/types/indices.ts
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+interface IndexModule {
+ number_of_shards: number | string;
+ codec: string;
+ routing_partition_size: number;
+ load_fixed_bitset_filters_eagerly: boolean;
+ shard: {
+ check_on_startup: boolean | 'checksum';
+ };
+ number_of_replicas: number;
+ auto_expand_replicas: false | string;
+ lifecycle: LifecycleModule;
+}
+
+interface AnalysisModule {
+ analyzer: {
+ [key: string]: {
+ type: string;
+ tokenizer: string;
+ char_filter?: string[];
+ filter?: string[];
+ position_increment_gap?: number;
+ };
+ };
+}
+
+interface LifecycleModule {
+ name: string;
+ rollover_alias?: string;
+ parse_origination_date?: boolean;
+ origination_date?: number;
+}
+
+export interface IndexSettings {
+ index?: Partial;
+ analysis?: AnalysisModule;
+ [key: string]: any;
+}
diff --git a/x-pack/plugins/index_management/common/types/mappings.ts b/x-pack/plugins/index_management/common/types/mappings.ts
new file mode 100644
index 00000000000000..0bd3e38ed07a5d
--- /dev/null
+++ b/x-pack/plugins/index_management/common/types/mappings.ts
@@ -0,0 +1,11 @@
+/*
+ * 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.
+ */
+
+// TODO: Move mappings type from Mappings editor here
+
+export interface Mappings {
+ [key: string]: any;
+}
diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts
index e31c10a775156a..c37088982f207f 100644
--- a/x-pack/plugins/index_management/common/types/templates.ts
+++ b/x-pack/plugins/index_management/common/types/templates.ts
@@ -4,6 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { IndexSettings } from './indices';
+import { Aliases } from './aliases';
+import { Mappings } from './mappings';
+
+// Template serialized (from Elasticsearch)
+interface TemplateBaseSerialized {
+ name: string;
+ index_patterns: string[];
+ version?: number;
+ order?: number;
+}
+
+export interface TemplateV1Serialized extends TemplateBaseSerialized {
+ settings?: IndexSettings;
+ aliases?: Aliases;
+ mappings?: Mappings;
+}
+
+export interface TemplateV2Serialized extends TemplateBaseSerialized {
+ template: {
+ settings?: IndexSettings;
+ aliases?: Aliases;
+ mappings?: Mappings;
+ };
+ priority?: number;
+ composed_of?: string[];
+}
+
+/**
+ * Interface for the template list in our UI table
+ * we don't include the mappings, settings and aliases
+ * to reduce the payload size sent back to the client.
+ */
export interface TemplateListItem {
name: string;
indexPatterns: string[];
@@ -16,37 +49,35 @@ export interface TemplateListItem {
name: string;
};
isManaged: boolean;
+ _kbnMeta: {
+ formatVersion: IndexTemplateFormatVersion;
+ };
}
-export interface Template {
+
+/**
+ * TemplateDeserialized falls back to index template V2 format
+ * The UI will only be dealing with this interface, conversion from and to V1 format
+ * is done server side.
+ */
+export interface TemplateDeserialized {
name: string;
indexPatterns: string[];
+ isManaged: boolean;
+ template: {
+ settings?: IndexSettings;
+ aliases?: Aliases;
+ mappings?: Mappings;
+ };
+ _kbnMeta: {
+ formatVersion: IndexTemplateFormatVersion;
+ };
version?: number;
+ priority?: number;
order?: number;
- settings?: object;
- aliases?: object;
- mappings?: object;
ilmPolicy?: {
name: string;
};
- isManaged: boolean;
+ composedOf?: string[];
}
-export interface TemplateEs {
- name: string;
- index_patterns: string[];
- version?: number;
- order?: number;
- settings?: {
- [key: string]: any;
- index?: {
- [key: string]: any;
- lifecycle?: {
- name: string;
- };
- };
- };
- aliases?: {
- [key: string]: any;
- };
- mappings?: object;
-}
+export type IndexTemplateFormatVersion = 1 | 2;
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx
index 298aab248f3673..2fe0e47b0147b7 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx
@@ -38,22 +38,20 @@ const setup = (props: any) =>
defaultProps: props,
})();
-const openModalWithJsonContent = ({ find, component }: TestBed) => async (json: any) => {
- find('load-json-button').simulate('click');
- component.update();
-
+const openModalWithJsonContent = ({ find, waitFor }: TestBed) => async (json: any) => {
// Set the mappings to load
- // @ts-ignore
await act(async () => {
+ find('load-json-button').simulate('click');
+ await waitFor('mockCodeEditor');
+
find('mockCodeEditor').simulate('change', {
jsonString: JSON.stringify(json),
});
- await nextTick(300); // There is a debounce in the JsonEditor that we need to wait for
+ await nextTick(500); // There is a debounce in the JsonEditor that we need to wait for
});
};
-// FLAKY: https://github.com/elastic/kibana/issues/59030
-describe.skip('', () => {
+describe('', () => {
test('it should forward valid mapping definition', async () => {
const mappingsToLoad = {
properties: {
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
index 9bd6e0428aacf6..e450ed525c7781 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
@@ -113,13 +113,14 @@ export const MappingsState = React.memo(
let nextState = state;
if (
+ state.fieldForm &&
state.documentFields.status === 'creatingField' &&
isValid &&
!bypassFieldFormValidation
) {
// If the form field is valid and we are creating a new field that has some data
// we automatically add the field to our state.
- const fieldFormData = state.fieldForm!.data.format() as Field;
+ const fieldFormData = state.fieldForm.data.format() as Field;
if (Object.keys(fieldFormData).length !== 0) {
nextState = addFieldToState(fieldFormData, state);
dispatch({ type: 'field.add', value: fieldFormData });
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
index 1e6733b1632d76..2f30363c1ff582 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts
@@ -33,11 +33,21 @@ export interface MappingsConfiguration {
}
export interface MappingsTemplates {
- dynamic_templates: Template[];
+ dynamic_templates: DynamicTemplate[];
}
-interface Template {
- [key: string]: any;
+interface DynamicTemplate {
+ [key: string]: {
+ mapping: {
+ [key: string]: any;
+ };
+ match_mapping_type?: string;
+ match?: string;
+ unmatch?: string;
+ match_pattern?: string;
+ path_match?: string;
+ path_unmatch?: string;
+ };
}
export interface MappingsFields {
diff --git a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx
index 4e36643dbe117b..b80e51d8d139ff 100644
--- a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx
@@ -4,28 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import React, { Fragment, useState } from 'react';
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';
+
+import { IndexTemplateFormatVersion } from '../../../common';
import { deleteTemplates } from '../services/api';
import { notificationService } from '../services/notification';
-import { Template } from '../../../common/types';
export const TemplateDeleteModal = ({
templatesToDelete,
callback,
}: {
- templatesToDelete: Array;
+ templatesToDelete: Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>;
callback: (data?: { hasDeletedTemplates: boolean }) => void;
}) => {
const [isDeleteConfirmed, setIsDeleteConfirmed] = useState(false);
const numTemplatesToDelete = templatesToDelete.length;
- const hasSystemTemplate = Boolean(
- templatesToDelete.find(templateName => templateName.startsWith('.'))
- );
+ const hasSystemTemplate = Boolean(templatesToDelete.find(({ name }) => name.startsWith('.')));
const handleDeleteTemplates = () => {
deleteTemplates(templatesToDelete).then(({ data: { templatesDeleted, errors }, error }) => {
@@ -38,7 +37,7 @@ export const TemplateDeleteModal = ({
'xpack.idxMgmt.deleteTemplatesModal.successDeleteSingleNotificationMessageText',
{
defaultMessage: "Deleted template '{templateName}'",
- values: { templateName: templatesToDelete[0] },
+ values: { templateName: templatesToDelete[0].name },
}
)
: i18n.translate(
@@ -120,10 +119,10 @@ export const TemplateDeleteModal = ({
- {templatesToDelete.map(template => (
- -
- {template}
- {template.startsWith('.') ? (
+ {templatesToDelete.map(({ name }) => (
+
-
+ {name}
+ {name.startsWith('.') ? (
{' '}
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx
index 8628b6d8b8d74f..50a32787c7a04f 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx
@@ -29,7 +29,7 @@ export const StepAliases: React.FunctionComponent = ({
}) => {
const { content, setContent, error } = useJsonStep({
prop: 'aliases',
- defaultValue: template.aliases,
+ defaultValue: template?.template.aliases,
setDataGetter,
onStepValidityChange,
});
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx
index d51d512429ea4c..cf9b57dcbcb14d 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx
@@ -15,7 +15,7 @@ import {
EuiText,
} from '@elastic/eui';
import { documentationService } from '../../../services/documentation';
-import { StepProps } from '../types';
+import { StepProps, DataGetterFunc } from '../types';
import { MappingsEditor, OnUpdateHandler, LoadMappingsFromJsonButton } from '../../mappings_editor';
export const StepMappings: React.FunctionComponent = ({
@@ -23,16 +23,23 @@ export const StepMappings: React.FunctionComponent = ({
setDataGetter,
onStepValidityChange,
}) => {
- const [mappings, setMappings] = useState(template.mappings);
+ const [mappings, setMappings] = useState(template?.template.mappings);
const onMappingsEditorUpdate = useCallback(
({ isValid, getData, validate }) => {
onStepValidityChange(isValid);
- setDataGetter(async () => {
+
+ const dataGetterFunc: DataGetterFunc = async () => {
const isMappingsValid = isValid === undefined ? await validate() : isValid;
const data = getData(isMappingsValid);
- return Promise.resolve({ isValid: isMappingsValid, data: { mappings: data } });
- });
+ return {
+ isValid: isMappingsValid,
+ data: { mappings: data },
+ path: 'template',
+ };
+ };
+
+ setDataGetter(dataGetterFunc);
},
[setDataGetter, onStepValidityChange]
);
@@ -96,7 +103,7 @@ export const StepMappings: React.FunctionComponent = ({
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx
index d6f259726548c1..06b905840b9da4 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx
@@ -22,11 +22,12 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { serializers } from '../../../../shared_imports';
-import { serializeTemplate } from '../../../../../common/lib/template_serialization';
-import { Template } from '../../../../../common/types';
-
+import {
+ serializeV1Template,
+ serializeV2Template,
+} from '../../../../../common/lib/template_serialization';
+import { TemplateDeserialized, getTemplateParameter } from '../../../../../common';
import { doMappingsHaveType } from '../../mappings_editor/lib';
-
import { StepProps } from '../types';
const { stripEmptyFields } = serializers;
@@ -55,16 +56,25 @@ const getDescriptionText = (data: any) => {
};
export const StepReview: React.FunctionComponent = ({ template, updateCurrentStep }) => {
- const { name, indexPatterns, version, order } = template;
+ const {
+ name,
+ indexPatterns,
+ version,
+ order,
+ _kbnMeta: { formatVersion },
+ } = template!;
+
+ const serializedTemplate =
+ formatVersion === 1
+ ? serializeV1Template(stripEmptyFields(template!) as TemplateDeserialized)
+ : serializeV2Template(stripEmptyFields(template!) as TemplateDeserialized);
- const serializedTemplate = serializeTemplate(stripEmptyFields(template) as Template);
// Name not included in ES request body
delete serializedTemplate.name;
- const {
- mappings: serializedMappings,
- settings: serializedSettings,
- aliases: serializedAliases,
- } = serializedTemplate;
+
+ const serializedMappings = getTemplateParameter(serializedTemplate, 'mappings');
+ const serializedSettings = getTemplateParameter(serializedTemplate, 'settings');
+ const serializedAliases = getTemplateParameter(serializedTemplate, 'aliases');
const numIndexPatterns = indexPatterns!.length;
@@ -162,7 +172,7 @@ export const StepReview: React.FunctionComponent = ({ template, updat
);
const RequestTab = () => {
- const includeTypeName = doMappingsHaveType(template.mappings);
+ const includeTypeName = doMappingsHaveType(template!.template.mappings);
const endpoint = `PUT _template/${name || ''}${
includeTypeName ? '?include_type_name' : ''
}`;
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx
index cead652c9f6fcb..7c1ee6388a6182 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx
@@ -29,7 +29,7 @@ export const StepSettings: React.FunctionComponent = ({
}) => {
const { content, setContent, error } = useJsonStep({
prop: 'settings',
- defaultValue: template.settings,
+ defaultValue: template?.template.settings,
setDataGetter,
onStepValidityChange,
});
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts b/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts
index fbe479ea0cf235..25dbe784db3a1f 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts
@@ -8,7 +8,7 @@ import { useEffect, useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { isJSON } from '../../../../shared_imports';
-import { StepProps } from '../types';
+import { StepProps, DataGetterFunc } from '../types';
interface Parameters {
prop: 'settings' | 'mappings' | 'aliases';
@@ -44,11 +44,13 @@ export const useJsonStep = ({
return isValid;
}, [content]);
- const dataGetter = useCallback(() => {
+ const dataGetter = useCallback(() => {
const isValid = validateContent();
const value = isValid && content.trim() !== '' ? JSON.parse(content) : {};
- const data = { [prop]: value };
- return Promise.resolve({ isValid, data });
+ // If no key has been added to the JSON object, we strip it out so an empty object is not sent in the request
+ const data = { [prop]: Object.keys(value).length > 0 ? value : undefined };
+
+ return Promise.resolve({ isValid, data, path: 'template' });
}, [content, validateContent, prop]);
useEffect(() => {
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
index 58be9b2c633653..f6193bc71aa912 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
@@ -15,7 +15,7 @@ import {
} from '@elastic/eui';
import { serializers } from '../../../shared_imports';
-import { Template } from '../../../../common/types';
+import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common';
import { TemplateSteps } from './template_steps';
import { StepAliases, StepLogistics, StepMappings, StepSettings, StepReview } from './steps';
import { StepProps, DataGetterFunc } from './types';
@@ -24,11 +24,11 @@ import { SectionError } from '../section_error';
const { stripEmptyFields } = serializers;
interface Props {
- onSave: (template: Template) => void;
+ onSave: (template: TemplateDeserialized) => void;
clearSaveError: () => void;
isSaving: boolean;
saveError: any;
- defaultValue?: Template;
+ defaultValue?: TemplateDeserialized;
isEditing?: boolean;
}
@@ -47,7 +47,15 @@ const stepComponentMap: { [key: number]: React.FunctionComponent } =
};
export const TemplateForm: React.FunctionComponent = ({
- defaultValue = { isManaged: false },
+ defaultValue = {
+ name: '',
+ indexPatterns: [],
+ template: {},
+ isManaged: false,
+ _kbnMeta: {
+ formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT,
+ },
+ },
onSave,
isSaving,
saveError,
@@ -63,7 +71,7 @@ export const TemplateForm: React.FunctionComponent = ({
5: defaultValidation,
});
- const template = useRef>(defaultValue);
+ const template = useRef(defaultValue);
const stepsDataGetters = useRef>({});
const lastStep = Object.keys(stepComponentMap).length;
@@ -91,17 +99,31 @@ export const TemplateForm: React.FunctionComponent = ({
);
const validateAndGetDataFromCurrentStep = async () => {
- const validateAndGetData = stepsDataGetters.current[currentStep];
+ const validateAndGetStepData = stepsDataGetters.current[currentStep];
- if (!validateAndGetData) {
+ if (!validateAndGetStepData) {
throw new Error(`No data getter has been set for step "${currentStep}"`);
}
- const { isValid, data } = await validateAndGetData();
+ const { isValid, data, path } = await validateAndGetStepData();
if (isValid) {
- // Update the template object
- template.current = { ...template.current, ...data };
+ // Update the template object with the current step data
+ if (path) {
+ // We only update a "slice" of the template
+ const sliceToUpdate = template.current[path as keyof TemplateDeserialized];
+
+ if (sliceToUpdate === null || typeof sliceToUpdate !== 'object') {
+ return { isValid, data };
+ }
+
+ template.current = {
+ ...template.current,
+ [path]: { ...sliceToUpdate, ...data },
+ };
+ } else {
+ template.current = { ...template.current, ...data };
+ }
}
return { isValid, data };
@@ -111,9 +133,9 @@ export const TemplateForm: React.FunctionComponent = ({
// All steps needs validation, except for the last step
const shouldValidate = currentStep !== lastStep;
- let isValid = isStepValid;
if (shouldValidate) {
- isValid = isValid === false ? false : (await validateAndGetDataFromCurrentStep()).isValid;
+ const isValid =
+ isStepValid === false ? false : (await validateAndGetDataFromCurrentStep()).isValid;
// If step is invalid do not let user proceed
if (!isValid) {
@@ -222,7 +244,10 @@ export const TemplateForm: React.FunctionComponent = ({
fill
color="secondary"
iconType="check"
- onClick={onSave.bind(null, stripEmptyFields(template.current) as Template)}
+ onClick={onSave.bind(
+ null,
+ stripEmptyFields(template.current!) as TemplateDeserialized
+ )}
data-test-subj="submitButton"
isLoading={isSaving}
>
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/types.ts b/x-pack/plugins/index_management/public/application/components/template_form/types.ts
index 9385f0c9f738b3..5db53e91ed261e 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/types.ts
+++ b/x-pack/plugins/index_management/public/application/components/template_form/types.ts
@@ -4,14 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Template } from '../../../../common/types';
+import { TemplateDeserialized } from '../../../../common';
export interface StepProps {
- template: Partial;
+ template?: TemplateDeserialized;
setDataGetter: (dataGetter: DataGetterFunc) => void;
updateCurrentStep: (step: number) => void;
onStepValidityChange: (isValid: boolean | undefined) => void;
isEditing?: boolean;
}
-export type DataGetterFunc = () => Promise<{ isValid: boolean; data: any }>;
+export type DataGetterFunc = () => Promise<{
+ /** Is the step data valid or not */
+ isValid: boolean;
+ /** The current step data (can be invalid) */
+ data: any;
+ /** Optional "slice" of the complete object the step is updating */
+ path?: string;
+}>;
diff --git a/x-pack/plugins/index_management/public/application/lib/index_templates.ts b/x-pack/plugins/index_management/public/application/lib/index_templates.ts
new file mode 100644
index 00000000000000..7129e536287c11
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/lib/index_templates.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { parse } from 'query-string';
+import { Location } from 'history';
+
+export const getFormatVersionFromQueryparams = (location: Location): 1 | 2 | undefined => {
+ const { v: version } = parse(location.search.substring(1));
+
+ if (!Boolean(version) || typeof version !== 'string') {
+ return undefined;
+ }
+
+ return +version as 1 | 2;
+};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx
index 421119bd8df960..fa7d734ad0d2ba 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx
@@ -7,14 +7,16 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
-import { Template } from '../../../../../../../common/types';
+import { TemplateDeserialized } from '../../../../../../../common';
interface Props {
- templateDetails: Template;
+ templateDetails: TemplateDeserialized;
}
export const TabAliases: React.FunctionComponent = ({ templateDetails }) => {
- const { aliases } = templateDetails;
+ const {
+ template: { aliases },
+ } = templateDetails;
if (aliases && Object.keys(aliases).length) {
return (
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx
index 83f2e67fb12c5e..6e0257c6b377bb 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx
@@ -7,14 +7,16 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
-import { Template } from '../../../../../../../common/types';
+import { TemplateDeserialized } from '../../../../../../../common';
interface Props {
- templateDetails: Template;
+ templateDetails: TemplateDeserialized;
}
export const TabMappings: React.FunctionComponent = ({ templateDetails }) => {
- const { mappings } = templateDetails;
+ const {
+ template: { mappings },
+ } = templateDetails;
if (mappings && Object.keys(mappings).length) {
return (
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx
index 8b2a431bee65a6..8f75c2cb77801b 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx
@@ -7,14 +7,16 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCodeBlock, EuiCallOut } from '@elastic/eui';
-import { Template } from '../../../../../../../common/types';
+import { TemplateDeserialized } from '../../../../../../../common';
interface Props {
- templateDetails: Template;
+ templateDetails: TemplateDeserialized;
}
export const TabSettings: React.FunctionComponent = ({ templateDetails }) => {
- const { settings } = templateDetails;
+ const {
+ template: { settings },
+ } = templateDetails;
if (settings && Object.keys(settings).length) {
return (
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
index 99f5db54b4ba23..9ce29ab746a2ff 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
@@ -14,11 +14,11 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
-import { Template } from '../../../../../../../common/types';
+import { TemplateDeserialized } from '../../../../../../../common';
import { getILMPolicyPath } from '../../../../../services/navigation';
interface Props {
- templateDetails: Template;
+ templateDetails: TemplateDeserialized;
}
const NoneDescriptionText = () => (
@@ -35,6 +35,7 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails })
return (
+ {/* Index patterns */}
= ({ templateDetails })
indexPatterns.toString()
)}
+
+ {/* // ILM Policy */}
= ({ templateDetails })
)}
+
+ {/* // Order */}
= ({ templateDetails })
{order || order === 0 ? order : }
+
+ {/* // Version */}
void;
- editTemplate: (templateName: Template['name']) => void;
- cloneTemplate: (templateName: Template['name']) => void;
+ editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void;
+ cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void;
reload: () => Promise;
}
@@ -79,7 +79,7 @@ const TABS = [
];
const tabToComponentMap: {
- [key: string]: React.FunctionComponent<{ templateDetails: Template }>;
+ [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>;
} = {
[SUMMARY_TAB_ID]: TabSummary,
[SETTINGS_TAB_ID]: TabSettings,
@@ -95,7 +95,7 @@ const tabToUiMetricMap: { [key: string]: string } = {
};
export const TemplateDetails: React.FunctionComponent = ({
- templateName,
+ template: { name: templateName, formatVersion },
onClose,
editTemplate,
cloneTemplate,
@@ -103,10 +103,14 @@ export const TemplateDetails: React.FunctionComponent = ({
}) => {
const { uiMetricService } = useServices();
const decodedTemplateName = decodePath(templateName);
- const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(decodedTemplateName);
- // TS complains if we use destructuring here. Fixed in 3.6.0 (https://github.com/microsoft/TypeScript/pull/31711).
- const isManaged = templateDetails ? templateDetails.isManaged : undefined;
- const [templateToDelete, setTemplateToDelete] = useState>([]);
+ const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(
+ decodedTemplateName,
+ formatVersion
+ );
+ const isManaged = templateDetails?.isManaged;
+ const [templateToDelete, setTemplateToDelete] = useState<
+ Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>
+ >([]);
const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID);
const [isPopoverOpen, setIsPopOverOpen] = useState(false);
@@ -275,7 +279,7 @@ export const TemplateDetails: React.FunctionComponent = ({
defaultMessage: 'Edit',
}),
icon: 'pencil',
- onClick: () => editTemplate(decodedTemplateName),
+ onClick: () => editTemplate(templateName, formatVersion),
disabled: isManaged,
},
{
@@ -283,7 +287,7 @@ export const TemplateDetails: React.FunctionComponent = ({
defaultMessage: 'Clone',
}),
icon: 'copy',
- onClick: () => cloneTemplate(decodedTemplateName),
+ onClick: () => cloneTemplate(templateName, formatVersion),
},
{
name: i18n.translate(
@@ -293,7 +297,8 @@ export const TemplateDetails: React.FunctionComponent = ({
}
),
icon: 'trash',
- onClick: () => setTemplateToDelete([decodedTemplateName]),
+ onClick: () =>
+ setTemplateToDelete([{ name: decodedTemplateName, formatVersion }]),
disabled: isManaged,
},
],
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
index ffdb224f162713..1e84202639ee84 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
@@ -16,31 +16,35 @@ import {
EuiFlexItem,
EuiFlexGroup,
} from '@elastic/eui';
+
+import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
+import { IndexTemplateFormatVersion } from '../../../../../common';
import { SectionError, SectionLoading, Error } from '../../../components';
-import { TemplateTable } from './template_table';
import { useLoadIndexTemplates } from '../../../services/api';
-import { Template } from '../../../../../common/types';
import { useServices } from '../../../app_context';
import {
getTemplateEditLink,
getTemplateListLink,
getTemplateCloneLink,
} from '../../../services/routing';
-import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
+import { getFormatVersionFromQueryparams } from '../../../lib/index_templates';
+import { TemplateTable } from './template_table';
import { TemplateDetails } from './template_details';
interface MatchParams {
- templateName?: Template['name'];
+ templateName?: string;
}
export const TemplateList: React.FunctionComponent> = ({
match: {
params: { templateName },
},
+ location,
history,
}) => {
const { uiMetricService } = useServices();
const { error, isLoading, data: templates, sendRequest: reload } = useLoadIndexTemplates();
+ const queryParamsFormatVersion = getFormatVersionFromQueryparams(location);
let content;
@@ -48,8 +52,7 @@ export const TemplateList: React.FunctionComponent
- templates ? templates.filter((template: Template) => !template.name.startsWith('.')) : [],
+ () => (templates ? templates.filter(template => !template.name.startsWith('.')) : []),
[templates]
);
@@ -57,12 +60,12 @@ export const TemplateList: React.FunctionComponent {
- history.push(getTemplateEditLink(name));
+ const editTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => {
+ history.push(getTemplateEditLink(name, formatVersion));
};
- const cloneTemplate = (name: Template['name']) => {
- history.push(getTemplateCloneLink(name));
+ const cloneTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => {
+ history.push(getTemplateCloneLink(name, formatVersion));
};
// Track component loaded
@@ -149,9 +152,12 @@ export const TemplateList: React.FunctionComponent
{content}
- {templateName && (
+ {templateName && queryParamsFormatVersion !== undefined && (
Promise;
- editTemplate: (name: Template['name']) => void;
- cloneTemplate: (name: Template['name']) => void;
+ editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void;
+ cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void;
}
export const TemplateTable: React.FunctionComponent = ({
@@ -30,7 +30,9 @@ export const TemplateTable: React.FunctionComponent = ({
}) => {
const { uiMetricService } = useServices();
const [selection, setSelection] = useState([]);
- const [templatesToDelete, setTemplatesToDelete] = useState>([]);
+ const [templatesToDelete, setTemplatesToDelete] = useState<
+ Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>
+ >([]);
const columns: Array> = [
{
@@ -40,11 +42,11 @@ export const TemplateTable: React.FunctionComponent = ({
}),
truncateText: true,
sortable: true,
- render: (name: TemplateListItem['name']) => {
+ render: (name: TemplateListItem['name'], item: TemplateListItem) => {
return (
/* eslint-disable-next-line @elastic/eui/href-or-on-click */
uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)}
>
@@ -133,10 +135,10 @@ export const TemplateTable: React.FunctionComponent = ({
}),
icon: 'pencil',
type: 'icon',
- onClick: ({ name }: Template) => {
- editTemplate(name);
+ onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => {
+ editTemplate(name, formatVersion);
},
- enabled: ({ isManaged }: Template) => !isManaged,
+ enabled: ({ isManaged }: TemplateListItem) => !isManaged,
},
{
type: 'icon',
@@ -147,8 +149,8 @@ export const TemplateTable: React.FunctionComponent = ({
defaultMessage: 'Clone this template',
}),
icon: 'copy',
- onClick: ({ name }: Template) => {
- cloneTemplate(name);
+ onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => {
+ cloneTemplate(name, formatVersion);
},
},
{
@@ -161,11 +163,11 @@ export const TemplateTable: React.FunctionComponent = ({
icon: 'trash',
color: 'danger',
type: 'icon',
- onClick: ({ name }: Template) => {
- setTemplatesToDelete([name]);
+ onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => {
+ setTemplatesToDelete([{ name, formatVersion }]);
},
isPrimary: true,
- enabled: ({ isManaged }: Template) => !isManaged,
+ enabled: ({ isManaged }: TemplateListItem) => !isManaged,
},
],
},
@@ -185,7 +187,7 @@ export const TemplateTable: React.FunctionComponent = ({
const selectionConfig = {
onSelectionChange: setSelection,
- selectable: ({ isManaged }: Template) => !isManaged,
+ selectable: ({ isManaged }: TemplateListItem) => !isManaged,
selectableMessage: (selectable: boolean) => {
if (!selectable) {
return i18n.translate('xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip', {
@@ -205,7 +207,12 @@ export const TemplateTable: React.FunctionComponent = ({
- setTemplatesToDelete(selection.map((selected: TemplateListItem) => selected.name))
+ setTemplatesToDelete(
+ selection.map(({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => ({
+ name,
+ formatVersion,
+ }))
+ )
}
color="danger"
>
diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
index cf6ca3c0657773..b69e441feb176d 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
@@ -7,11 +7,13 @@ 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 { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common';
import { TemplateForm, SectionLoading, SectionError, Error } from '../../components';
import { breadcrumbService } from '../../services/breadcrumbs';
import { decodePath, getTemplateDetailsLink } from '../../services/routing';
-import { Template } from '../../../../common/types';
import { saveTemplate, useLoadIndexTemplate } from '../../services/api';
+import { getFormatVersionFromQueryparams } from '../../lib/index_templates';
interface MatchParams {
name: string;
@@ -21,17 +23,21 @@ export const TemplateClone: React.FunctionComponent {
const decodedTemplateName = decodePath(name);
+ const formatVersion =
+ getFormatVersionFromQueryparams(location) ?? DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT;
+
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
-
const { error: templateToCloneError, data: templateToClone, isLoading } = useLoadIndexTemplate(
- decodedTemplateName
+ decodedTemplateName,
+ formatVersion
);
- const onSave = async (template: Template) => {
+ const onSave = async (template: TemplateDeserialized) => {
setIsSaving(true);
setSaveError(null);
@@ -46,7 +52,7 @@ export const TemplateClone: React.FunctionComponent {
@@ -85,7 +91,7 @@ export const TemplateClone: React.FunctionComponent = ({ h
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
- const onSave = async (template: Template) => {
+ const onSave = async (template: TemplateDeserialized) => {
const { name } = template;
setIsSaving(true);
@@ -32,7 +33,7 @@ export const TemplateCreate: React.FunctionComponent = ({ h
return;
}
- history.push(getTemplateDetailsLink(name));
+ history.push(getTemplateDetailsLink(name, template._kbnMeta.formatVersion));
};
const clearSaveError = () => {
diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
index 1e9d5f294de34c..9ad26d0af802d8 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
@@ -7,11 +7,13 @@ 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 { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common';
import { breadcrumbService } from '../../services/breadcrumbs';
import { useLoadIndexTemplate, updateTemplate } from '../../services/api';
import { decodePath, getTemplateDetailsLink } from '../../services/routing';
import { SectionLoading, SectionError, TemplateForm, Error } from '../../components';
-import { Template } from '../../../../common/types';
+import { getFormatVersionFromQueryparams } from '../../lib/index_templates';
interface MatchParams {
name: string;
@@ -21,19 +23,26 @@ export const TemplateEdit: React.FunctionComponent {
const decodedTemplateName = decodePath(name);
+ const formatVersion =
+ getFormatVersionFromQueryparams(location) ?? DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT;
+
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
- const { error, data: template, isLoading } = useLoadIndexTemplate(decodedTemplateName);
+ const { error, data: template, isLoading } = useLoadIndexTemplate(
+ decodedTemplateName,
+ formatVersion
+ );
useEffect(() => {
breadcrumbService.setBreadcrumbs('templateEdit');
}, []);
- const onSave = async (updatedTemplate: Template) => {
+ const onSave = async (updatedTemplate: TemplateDeserialized) => {
setIsSaving(true);
setSaveError(null);
@@ -46,7 +55,7 @@ export const TemplateEdit: React.FunctionComponent {
diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts
index 4034897ebdd6c5..395220a6c56fc5 100644
--- a/x-pack/plugins/index_management/public/application/services/api.ts
+++ b/x-pack/plugins/index_management/public/application/services/api.ts
@@ -37,7 +37,11 @@ import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants';
import { useRequest, sendRequest } from './use_request';
import { httpService } from './http';
import { UiMetricService } from './ui_metric';
-import { Template } from '../../../common/types';
+import {
+ TemplateDeserialized,
+ TemplateListItem,
+ IndexTemplateFormatVersion,
+} from '../../../common';
import { doMappingsHaveType } from '../components/mappings_editor';
import { IndexMgmtMetricsType } from '../../types';
@@ -202,34 +206,43 @@ export async function loadIndexData(type: string, indexName: string) {
}
export function useLoadIndexTemplates() {
- return useRequest({
+ return useRequest({
path: `${API_BASE_PATH}/templates`,
method: 'get',
});
}
-export async function deleteTemplates(names: Array) {
+export async function deleteTemplates(
+ templates: Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>
+) {
const result = sendRequest({
- path: `${API_BASE_PATH}/templates/${names.map(name => encodeURIComponent(name)).join(',')}`,
- method: 'delete',
+ path: `${API_BASE_PATH}/delete-templates`,
+ method: 'post',
+ body: { templates },
});
- const uimActionType = names.length > 1 ? UIM_TEMPLATE_DELETE_MANY : UIM_TEMPLATE_DELETE;
+ const uimActionType = templates.length > 1 ? UIM_TEMPLATE_DELETE_MANY : UIM_TEMPLATE_DELETE;
uiMetricService.trackMetric('count', uimActionType);
return result;
}
-export function useLoadIndexTemplate(name: Template['name']) {
- return useRequest({
+export function useLoadIndexTemplate(
+ name: TemplateDeserialized['name'],
+ formatVersion: IndexTemplateFormatVersion
+) {
+ return useRequest({
path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`,
method: 'get',
+ query: {
+ v: formatVersion,
+ },
});
}
-export async function saveTemplate(template: Template, isClone?: boolean) {
- const includeTypeName = doMappingsHaveType(template.mappings);
+export async function saveTemplate(template: TemplateDeserialized, isClone?: boolean) {
+ const includeTypeName = doMappingsHaveType(template.template.mappings);
const result = await sendRequest({
path: `${API_BASE_PATH}/templates`,
method: 'put',
@@ -246,8 +259,8 @@ export async function saveTemplate(template: Template, isClone?: boolean) {
return result;
}
-export async function updateTemplate(template: Template) {
- const includeTypeName = doMappingsHaveType(template.mappings);
+export async function updateTemplate(template: TemplateDeserialized) {
+ const includeTypeName = doMappingsHaveType(template.template.mappings);
const { name } = template;
const result = await sendRequest({
path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`,
diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts
index aceadab18b1ff4..a6d8f67751cd10 100644
--- a/x-pack/plugins/index_management/public/application/services/routing.ts
+++ b/x-pack/plugins/index_management/public/application/services/routing.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BASE_PATH } from '../../../common/constants';
+import { IndexTemplateFormatVersion } from '../../../common';
export const getTemplateListLink = () => {
return `${BASE_PATH}templates`;
@@ -11,18 +12,28 @@ export const getTemplateListLink = () => {
// 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))}`;
+export const getTemplateDetailsLink = (
+ name: string,
+ formatVersion: IndexTemplateFormatVersion,
+ withHash = false
+) => {
+ const baseUrl = `${BASE_PATH}templates/${encodeURIComponent(
+ encodeURIComponent(name)
+ )}?v=${formatVersion}`;
const url = withHash ? `#${baseUrl}` : baseUrl;
return encodeURI(url);
};
-export const getTemplateEditLink = (name: string) => {
- return encodeURI(`${BASE_PATH}edit_template/${encodeURIComponent(encodeURIComponent(name))}`);
+export const getTemplateEditLink = (name: string, formatVersion: IndexTemplateFormatVersion) => {
+ return encodeURI(
+ `${BASE_PATH}edit_template/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}`
+ );
};
-export const getTemplateCloneLink = (name: string) => {
- return encodeURI(`${BASE_PATH}clone_template/${encodeURIComponent(encodeURIComponent(name))}`);
+export const getTemplateCloneLink = (name: string, formatVersion: IndexTemplateFormatVersion) => {
+ return encodeURI(
+ `${BASE_PATH}clone_template/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}`
+ );
};
export const decodePath = (pathname: string): string => {
diff --git a/x-pack/plugins/index_management/public/application/services/use_request.ts b/x-pack/plugins/index_management/public/application/services/use_request.ts
index 8ede3d196911cc..b87a6db20f0bf8 100644
--- a/x-pack/plugins/index_management/public/application/services/use_request.ts
+++ b/x-pack/plugins/index_management/public/application/services/use_request.ts
@@ -18,6 +18,6 @@ export const sendRequest = (config: SendRequestConfig): Promise {
- return _useRequest(httpService.httpClient, config);
+export const useRequest = (config: UseRequestConfig) => {
+ return _useRequest(httpService.httpClient, config);
};
diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts
index 46a9ab9f329814..26dd96eb1ed198 100644
--- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts
+++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts
@@ -7,8 +7,8 @@
import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
-import { Template, TemplateEs } from '../../../../common/types';
-import { serializeTemplate } from '../../../../common/lib';
+import { TemplateDeserialized } from '../../../../common';
+import { serializeV1Template } from '../../../../common/lib';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
import { templateSchema } from './validate_schemas';
@@ -23,9 +23,19 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies)
{ path: addBasePath('/templates'), validate: { body: bodySchema, query: querySchema } },
license.guardApiRoute(async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient;
- const template = req.body as Template;
const { include_type_name } = req.query as TypeOf;
- const serializedTemplate = serializeTemplate(template) as TemplateEs;
+ const template = req.body as TemplateDeserialized;
+ const {
+ _kbnMeta: { formatVersion },
+ } = template;
+
+ if (formatVersion !== 1) {
+ return res.badRequest({ body: 'Only index template version 1 can be created.' });
+ }
+
+ // For now we format to V1 index templates.
+ // When the V2 API is ready we will only create V2 template format.
+ const serializedTemplate = serializeV1Template(template);
const {
name,
diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts
index c9f1995204d8c3..4c8fdd0c2f1c74 100644
--- a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts
+++ b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts
@@ -4,35 +4,47 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { schema } from '@kbn/config-schema';
+import { schema, TypeOf } from '@kbn/config-schema';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
import { wrapEsError } from '../../helpers';
-import { Template } from '../../../../common/types';
+import { TemplateDeserialized } from '../../../../common';
-const paramsSchema = schema.object({
- names: schema.string(),
+const bodySchema = schema.object({
+ templates: schema.arrayOf(
+ schema.object({
+ name: schema.string(),
+ formatVersion: schema.oneOf([schema.literal(1), schema.literal(2)]),
+ })
+ ),
});
export function registerDeleteRoute({ router, license }: RouteDependencies) {
- router.delete(
- { path: addBasePath('/templates/{names}'), validate: { params: paramsSchema } },
+ router.post(
+ {
+ path: addBasePath('/delete-templates'),
+ validate: { body: bodySchema },
+ },
license.guardApiRoute(async (ctx, req, res) => {
- const { names } = req.params as typeof paramsSchema.type;
- const templateNames = names.split(',');
- const response: { templatesDeleted: Array; errors: any[] } = {
+ const { templates } = req.body as TypeOf;
+ const response: { templatesDeleted: Array; errors: any[] } = {
templatesDeleted: [],
errors: [],
};
await Promise.all(
- templateNames.map(async name => {
+ templates.map(async ({ name, formatVersion }) => {
try {
+ if (formatVersion !== 1) {
+ return res.badRequest({ body: 'Only index template version 1 can be deleted.' });
+ }
+
await ctx.core.elasticsearch.dataClient.callAsCurrentUser('indices.deleteTemplate', {
name,
});
+
return response.templatesDeleted.push(name);
} catch (e) {
return response.errors.push({
diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
index 1fa2d6aa96f8c3..676bbbe7c0b57a 100644
--- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
+++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts
@@ -3,9 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { schema } from '@kbn/config-schema';
+import { schema, TypeOf } from '@kbn/config-schema';
-import { deserializeTemplate, deserializeTemplateList } from '../../../../common/lib';
+import { deserializeV1Template, deserializeTemplateList } from '../../../../common/lib';
import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
@@ -20,8 +20,9 @@ export function registerGetAllRoute({ router, license }: RouteDependencies) {
const indexTemplatesByName = await callAsCurrentUser('indices.getTemplate', {
include_type_name: true,
});
+ const body = deserializeTemplateList(indexTemplatesByName, managedTemplatePrefix);
- return res.ok({ body: deserializeTemplateList(indexTemplatesByName, managedTemplatePrefix) });
+ return res.ok({ body });
})
);
}
@@ -30,13 +31,27 @@ const paramsSchema = schema.object({
name: schema.string(),
});
+// Require the template format version (V1 or V2) to be provided as Query param
+const querySchema = schema.object({
+ v: schema.oneOf([schema.literal('1'), schema.literal('2')]),
+});
+
export function registerGetOneRoute({ router, license, lib }: RouteDependencies) {
router.get(
- { path: addBasePath('/templates/{name}'), validate: { params: paramsSchema } },
+ {
+ path: addBasePath('/templates/{name}'),
+ validate: { params: paramsSchema, query: querySchema },
+ },
license.guardApiRoute(async (ctx, req, res) => {
const { name } = req.params as typeof paramsSchema.type;
const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient;
+ const { v: version } = req.query as TypeOf;
+
+ if (version !== '1') {
+ return res.badRequest({ body: 'Only index template version 1 can be fetched.' });
+ }
+
try {
const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser);
const indexTemplateByName = await callAsCurrentUser('indices.getTemplate', {
@@ -46,7 +61,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies)
if (indexTemplateByName[name]) {
return res.ok({
- body: deserializeTemplate(
+ body: deserializeV1Template(
{ ...indexTemplateByName[name], name },
managedTemplatePrefix
),
diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts
index a79ddbddc65208..f829dfb249eba1 100644
--- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts
+++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts
@@ -5,8 +5,8 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
-import { Template, TemplateEs } from '../../../../common/types';
-import { serializeTemplate } from '../../../../common/lib';
+import { TemplateDeserialized } from '../../../../common';
+import { serializeV1Template } from '../../../../common/lib';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
import { templateSchema } from './validate_schemas';
@@ -29,8 +29,16 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies)
const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient;
const { name } = req.params as typeof paramsSchema.type;
const { include_type_name } = req.query as TypeOf;
- const template = req.body as Template;
- const serializedTemplate = serializeTemplate(template) as TemplateEs;
+ const template = req.body as TemplateDeserialized;
+ const {
+ _kbnMeta: { formatVersion },
+ } = template;
+
+ if (formatVersion !== 1) {
+ return res.badRequest({ body: 'Only index template version 1 can be edited.' });
+ }
+
+ const serializedTemplate = serializeV1Template(template);
const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate;
diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts
index 8bf2774ac38b37..491a686f811779 100644
--- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts
+++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts
@@ -11,9 +11,13 @@ export const templateSchema = schema.object({
indexPatterns: schema.arrayOf(schema.string()),
version: schema.maybe(schema.number()),
order: schema.maybe(schema.number()),
- settings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
- aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })),
- mappings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+ template: schema.maybe(
+ schema.object({
+ settings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+ aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+ mappings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+ })
+ ),
ilmPolicy: schema.maybe(
schema.object({
name: schema.maybe(schema.string()),
@@ -21,4 +25,7 @@ export const templateSchema = schema.object({
})
),
isManaged: schema.maybe(schema.boolean()),
+ _kbnMeta: schema.object({
+ formatVersion: schema.oneOf([schema.literal(1), schema.literal(2)]),
+ }),
});
diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts
index 19a6e0c2576470..055c32d5cd5e4a 100644
--- a/x-pack/plugins/index_management/test/fixtures/template.ts
+++ b/x-pack/plugins/index_management/test/fixtures/template.ts
@@ -5,24 +5,32 @@
*/
import { getRandomString, getRandomNumber } from '../../../../test_utils';
-import { Template } from '../../common/types';
+import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../common';
export const getTemplate = ({
name = getRandomString(),
version = getRandomNumber(),
order = getRandomNumber(),
indexPatterns = [],
- settings,
- aliases,
- mappings,
+ template: { settings, aliases, mappings } = {},
isManaged = false,
-}: Partial = {}): Template => ({
+ templateFormatVersion = DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT,
+}: Partial<
+ TemplateDeserialized & {
+ templateFormatVersion?: 1 | 2;
+ }
+> = {}): TemplateDeserialized => ({
name,
version,
order,
indexPatterns,
- settings,
- aliases,
- mappings,
+ template: {
+ aliases,
+ mappings,
+ settings,
+ },
isManaged,
+ _kbnMeta: {
+ formatVersion: templateFormatVersion,
+ },
});
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 d409d66e3459c3..5d8364a8b92c23 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
@@ -9,37 +9,43 @@ import { API_BASE_PATH, INDEX_PATTERNS } from './constants';
export const registerHelpers = ({ supertest }) => {
const getAllTemplates = () => supertest.get(`${API_BASE_PATH}/templates`);
- const getOneTemplate = name => supertest.get(`${API_BASE_PATH}/templates/${name}`);
+ const getOneTemplate = (name, formatVersion = 1) =>
+ supertest.get(`${API_BASE_PATH}/templates/${name}?v=${formatVersion}`);
- const getTemplatePayload = name => ({
+ const getTemplatePayload = (name, formatVersion = 1) => ({
name,
order: 1,
indexPatterns: INDEX_PATTERNS,
version: 1,
- settings: {
- number_of_shards: 1,
- index: {
- lifecycle: {
- name: 'my_policy',
+ template: {
+ settings: {
+ number_of_shards: 1,
+ index: {
+ lifecycle: {
+ name: 'my_policy',
+ },
},
},
- },
- mappings: {
- _source: {
- enabled: false,
- },
- properties: {
- host_name: {
- type: 'keyword',
+ mappings: {
+ _source: {
+ enabled: false,
},
- created_at: {
- type: 'date',
- format: 'EEE MMM dd HH:mm:ss Z yyyy',
+ properties: {
+ host_name: {
+ type: 'keyword',
+ },
+ created_at: {
+ type: 'date',
+ format: 'EEE MMM dd HH:mm:ss Z yyyy',
+ },
},
},
+ aliases: {
+ alias1: {},
+ },
},
- aliases: {
- alias1: {},
+ _kbnMeta: {
+ formatVersion,
},
});
@@ -49,14 +55,11 @@ export const registerHelpers = ({ supertest }) => {
.set('kbn-xsrf', 'xxx')
.send(payload);
- const deleteTemplates = templatesToDelete =>
+ const deleteTemplates = templates =>
supertest
- .delete(
- `${API_BASE_PATH}/templates/${templatesToDelete
- .map(template => encodeURIComponent(template))
- .join(',')}`
- )
- .set('kbn-xsrf', 'xxx');
+ .post(`${API_BASE_PATH}/delete-templates`)
+ .set('kbn-xsrf', 'xxx')
+ .send({ templates });
const updateTemplate = (payload, templateName) =>
supertest
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 d9344846ebb911..63bfd494301b3e 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
@@ -35,7 +35,7 @@ export default function({ getService }) {
await createTemplate(payload).expect(200);
});
- it('should list all the index templates with the expected properties', async () => {
+ it('should list all the index templates with the expected parameters', async () => {
const { body: templates } = await getAllTemplates().expect(200);
const createdTemplate = templates.find(template => template.name === payload.name);
@@ -46,8 +46,13 @@ export default function({ getService }) {
'hasAliases',
'hasMappings',
'ilmPolicy',
- ];
- expectedKeys.forEach(key => expect(Object.keys(createdTemplate).includes(key)).to.be(true));
+ 'isManaged',
+ 'order',
+ 'version',
+ '_kbnMeta',
+ ].sort();
+
+ expect(Object.keys(createdTemplate).sort()).to.eql(expectedKeys);
});
});
@@ -59,19 +64,23 @@ export default function({ getService }) {
await createTemplate(payload).expect(200);
});
- it('should list the index template with the expected properties', async () => {
+ it('should return the index template with the expected parameters', async () => {
const { body } = await getOneTemplate(templateName).expect(200);
const expectedKeys = [
'name',
'indexPatterns',
- 'settings',
- 'aliases',
- 'mappings',
+ 'template',
'ilmPolicy',
- ];
+ 'isManaged',
+ 'order',
+ 'version',
+ '_kbnMeta',
+ ].sort();
+ const expectedTemplateKeys = ['aliases', 'mappings', 'settings'].sort();
expect(body.name).to.equal(templateName);
- expectedKeys.forEach(key => expect(Object.keys(body).includes(key)).to.be(true));
+ expect(Object.keys(body).sort()).to.eql(expectedKeys);
+ expect(Object.keys(body.template).sort()).to.eql(expectedTemplateKeys);
});
});
@@ -145,7 +154,9 @@ export default function({ getService }) {
templateName
);
- const { body } = await deleteTemplates([templateName]).expect(200);
+ const { body } = await deleteTemplates([
+ { name: templateName, formatVersion: payload._kbnMeta.formatVersion },
+ ]).expect(200);
expect(body.errors).to.be.empty;
expect(body.templatesDeleted[0]).to.equal(templateName);
diff --git a/x-pack/test_utils/testbed/testbed.ts b/x-pack/test_utils/testbed/testbed.ts
index f32fb42a8a8b0f..2bd53adda2cdf1 100644
--- a/x-pack/test_utils/testbed/testbed.ts
+++ b/x-pack/test_utils/testbed/testbed.ts
@@ -138,6 +138,44 @@ export const registerTestBed = (
});
};
+ const waitFor: TestBed['waitFor'] = async (testSubject: T) => {
+ const triggeredAt = Date.now();
+
+ /**
+ * The way jest run tests in parallel + the not deterministic DOM update from React "hooks"
+ * add flakiness to the tests. This is especially true for component integration tests that
+ * make many update to the DOM.
+ *
+ * For this reason, when we _know_ that an element should be there after we updated some state,
+ * we will give it 30 seconds to appear in the DOM, checking every 100 ms for its presence.
+ */
+ const MAX_WAIT_TIME = 30000;
+ const WAIT_INTERVAL = 100;
+
+ const process = async (): Promise => {
+ const elemFound = exists(testSubject);
+
+ if (elemFound) {
+ // Great! nothing else to do here.
+ return;
+ }
+
+ const timeElapsed = Date.now() - triggeredAt;
+ if (timeElapsed > MAX_WAIT_TIME) {
+ throw new Error(
+ `I waited patiently for the "${testSubject}" test subject to appear with no luck. It is nowhere to be found!`
+ );
+ }
+
+ return new Promise(resolve => setTimeout(resolve, WAIT_INTERVAL)).then(() => {
+ component.update();
+ return process();
+ });
+ };
+
+ return process();
+ };
+
/**
* ----------------------------------------------------------------
* Forms
@@ -254,6 +292,7 @@ export const registerTestBed = (
exists,
find,
setProps,
+ waitFor,
table: {
getMetaData,
},
diff --git a/x-pack/test_utils/testbed/types.ts b/x-pack/test_utils/testbed/types.ts
index c51e6a256f66f7..e4bb3ee57adec1 100644
--- a/x-pack/test_utils/testbed/types.ts
+++ b/x-pack/test_utils/testbed/types.ts
@@ -55,6 +55,12 @@ export interface TestBed {
* @param updatedProps The updated prop object
*/
setProps: (updatedProps: any) => void;
+ /**
+ * Helper to wait until an element appears in the DOM as hooks updates cycles are tricky.
+ * Useful when loading a component that fetches a resource from the server
+ * and we need to wait for the data to be fetched (and bypass any "loading" state).
+ */
+ waitFor: (testSubject: T) => Promise;
form: {
/**
* Set the value of a form text input.