diff --git a/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-darwin.zip b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-darwin.zip new file mode 100644 index 0000000000000..07cd5365424be Binary files /dev/null and b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-darwin.zip differ diff --git a/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-linux.zip b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-linux.zip new file mode 100644 index 0000000000000..9d5a18ff3a906 Binary files /dev/null and b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-linux.zip differ diff --git a/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-windows.zip b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-windows.zip new file mode 100644 index 0000000000000..43aa95c8f14b3 Binary files /dev/null and b/x-pack/legacy/plugins/reporting/.chromium/chromium-312d84c-windows.zip differ diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index e5bce31ee6de1..da461609f0b83 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -12,7 +12,7 @@ type HttpResponse = Record | any[]; // Register helpers to mock HTTP Requests const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const setLoadTemplatesResponse = (response: HttpResponse = []) => { - server.respondWith('GET', `${API_BASE_PATH}/templates`, [ + server.respondWith('GET', `${API_BASE_PATH}/index-templates`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(response), @@ -28,7 +28,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; const setDeleteTemplateResponse = (response: HttpResponse = []) => { - server.respondWith('DELETE', `${API_BASE_PATH}/templates`, [ + server.respondWith('POST', `${API_BASE_PATH}/delete-index-templates`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(response), @@ -39,7 +39,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const status = error ? error.status || 400 : 200; const body = error ? error.body : response; - server.respondWith('GET', `${API_BASE_PATH}/templates/:id`, [ + server.respondWith('GET', `${API_BASE_PATH}/index-templates/:id`, [ status, { 'Content-Type': 'application/json' }, JSON.stringify(body), @@ -50,7 +50,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const status = error ? error.body.status || 400 : 200; const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - server.respondWith('PUT', `${API_BASE_PATH}/templates`, [ + server.respondWith('POST', `${API_BASE_PATH}/index-templates`, [ status, { 'Content-Type': 'application/json' }, body, @@ -61,7 +61,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const status = error ? error.status || 400 : 200; const body = error ? JSON.stringify(error.body) : JSON.stringify(response); - server.respondWith('PUT', `${API_BASE_PATH}/templates/:name`, [ + server.respondWith('PUT', `${API_BASE_PATH}/index-templates/:name`, [ status, { 'Content-Type': 'application/json' }, body, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts index 18e5edb5c2250..8e7755a65af3c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts @@ -16,6 +16,7 @@ export type TestSubjects = | 'cell' | 'closeDetailsButton' | 'createTemplateButton' + | 'createLegacyTemplateButton' | 'deleteSystemTemplateCallOut' | 'deleteTemplateButton' | 'deleteTemplatesConfirmation' @@ -46,4 +47,7 @@ export type TestSubjects = | 'templateDetails.title' | 'templateList' | 'templateTable' - | 'templatesTab'; + | 'templatesTab' + | 'legacyTemplateTable' + | 'viewButton' + | 'filterList.filterItem'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts index d195ce46c2f54..a7ac2ebf9bb02 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts @@ -7,9 +7,17 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment, nextTick } from '../helpers'; - import { HomeTestBed, setup } from './home.helpers'; +/** + * The below import is required to avoid a console error warn from the "brace" package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import { stubWebWorker } from '../../../../../test_utils/stub_web_worker'; +stubWebWorker(); + describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: HomeTestBed; @@ -62,7 +70,7 @@ describe('', () => { expect(exists('indicesList')).toBe(true); expect(exists('templateList')).toBe(false); - httpRequestsMockHelpers.setLoadTemplatesResponse([]); + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); actions.selectHomeTab('templatesTab'); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts index 5260dc64d0c91..98bd3077670a7 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -12,7 +12,6 @@ import { TestBed, TestBedConfig, findTestSubject, - nextTick, } from '../../../../../test_utils'; // NOTE: We have to use the Home component instead of the TemplateList component because we depend // upon react router to provide the name of the template to load in the detail panel. @@ -45,6 +44,7 @@ export interface IndexTemplatesTabTestBed extends TestBed { clickTemplateAt: (index: number) => void; clickCloseDetailsButton: () => void; clickActionMenu: (name: TemplateDeserialized['name']) => void; + toggleViewItem: (view: 'composable' | 'system') => void; }; } @@ -102,15 +102,14 @@ export const setup = async (): Promise => { const clickTemplateAt = async (index: number) => { const { component, table, router } = testBed; - const { rows } = table.getMetaData('templateTable'); + const { rows } = table.getMetaData('legacyTemplateTable'); const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); + const { href } = templateLink.props(); await act(async () => { - const { href } = templateLink.props(); router.navigateTo(href!); - await nextTick(); - component.update(); }); + component.update(); }; const clickCloseDetailsButton = () => { @@ -119,6 +118,23 @@ export const setup = async (): Promise => { find('closeDetailsButton').simulate('click'); }; + const toggleViewItem = (view: 'composable' | 'system') => { + const { find, component } = testBed; + const views = ['composable', 'system']; + + // First open the pop over + act(() => { + find('viewButton').simulate('click'); + }); + component.update(); + + // Then click on a filter item + act(() => { + find('filterList.filterItem').at(views.indexOf(view)).simulate('click'); + }); + component.update(); + }; + return { ...testBed, findAction, @@ -130,6 +146,7 @@ export const setup = async (): Promise => { clickTemplateAt, clickCloseDetailsButton, clickActionMenu, + toggleViewItem, }, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index c9a279e90d0e0..8f6a8dddeb195 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -8,16 +8,18 @@ import { act } from 'react-dom/test-utils'; import * as fixtures from '../../../test/fixtures'; import { API_BASE_PATH } from '../../../common/constants'; -import { setupEnvironment, nextTick, getRandomString } from '../helpers'; +import { setupEnvironment, getRandomString } from '../helpers'; import { IndexTemplatesTabTestBed, setup } from './index_templates_tab.helpers'; const removeWhiteSpaceOnArrayValues = (array: any[]) => array.map((value) => { - if (!value.trim) { + if (typeof value !== 'string') { return value; } - return value.trim(); + + // Convert non breaking spaces ( ) to ordinary space + return value.trim().replace(/\s/g, ' '); }); describe('Index Templates tab', () => { @@ -31,13 +33,8 @@ describe('Index Templates tab', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadIndicesResponse([]); - testBed = await setup(); - await act(async () => { - const { component } = testBed; - - await nextTick(); - component.update(); + testBed = await setup(); }); }); @@ -45,14 +42,12 @@ describe('Index Templates tab', () => { beforeEach(async () => { const { actions, component } = testBed; - httpRequestsMockHelpers.setLoadTemplatesResponse([]); - - actions.goToTemplatesList(); + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); await act(async () => { - await nextTick(); - component.update(); + actions.goToTemplatesList(); }); + component.update(); }); test('should display an empty prompt', async () => { @@ -64,6 +59,9 @@ describe('Index Templates tab', () => { }); describe('when there are index templates', () => { + // Add a default loadIndexTemplate response + httpRequestsMockHelpers.setLoadTemplateResponse(fixtures.getTemplate()); + const template1 = fixtures.getTemplate({ name: `a${getRandomString()}`, indexPatterns: ['template1Pattern1*', 'template1Pattern2'], @@ -78,37 +76,87 @@ describe('Index Templates tab', () => { }, }, }); + const template2 = fixtures.getTemplate({ name: `b${getRandomString()}`, indexPatterns: ['template2Pattern1*'], }); + const template3 = fixtures.getTemplate({ name: `.c${getRandomString()}`, // mock system template indexPatterns: ['template3Pattern1*', 'template3Pattern2', 'template3Pattern3'], }); + const template4 = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template4Pattern1*', 'template4Pattern2'], + template: { + settings: { + index: { + number_of_shards: '1', + lifecycle: { + name: 'my_ilm_policy', + }, + }, + }, + }, + isLegacy: true, + }); + + const template5 = fixtures.getTemplate({ + name: `b${getRandomString()}`, + indexPatterns: ['template5Pattern1*'], + isLegacy: true, + }); + + const template6 = fixtures.getTemplate({ + name: `.c${getRandomString()}`, // mock system template + indexPatterns: ['template6Pattern1*', 'template6Pattern2', 'template6Pattern3'], + isLegacy: true, + }); + const templates = [template1, template2, template3]; + const legacyTemplates = [template4, template5, template6]; beforeEach(async () => { const { actions, component } = testBed; - httpRequestsMockHelpers.setLoadTemplatesResponse(templates); - - actions.goToTemplatesList(); + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates, legacyTemplates }); await act(async () => { - await nextTick(); - component.update(); + actions.goToTemplatesList(); }); + component.update(); }); test('should list them in the table', async () => { const { table } = testBed; const { tableCellsValues } = table.getMetaData('templateTable'); + const { tableCellsValues: legacyTableCellsValues } = table.getMetaData('legacyTemplateTable'); + // Test composable table content tableCellsValues.forEach((row, i) => { const template = templates[i]; + const { name, indexPatterns, priority, ilmPolicy, composedOf } = template; + + const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; + const composedOfString = composedOf ? composedOf.join(',') : ''; + const priorityFormatted = priority ? priority.toString() : ''; + + expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + name, + indexPatterns.join(', '), + ilmPolicyName, + composedOfString, + priorityFormatted, + 'M S A', // Mappings Settings Aliases badges + ]); + }); + + // Test legacy table content + legacyTableCellsValues.forEach((row, i) => { + const template = legacyTemplates[i]; const { name, indexPatterns, order, ilmPolicy } = template; const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; @@ -129,44 +177,40 @@ describe('Index Templates tab', () => { }); test('should have a button to reload the index templates', async () => { - const { component, exists, actions } = testBed; + const { exists, actions } = testBed; const totalRequests = server.requests.length; expect(exists('reloadButton')).toBe(true); await act(async () => { actions.clickReloadButton(); - await nextTick(); - component.update(); }); expect(server.requests.length).toBe(totalRequests + 1); - expect(server.requests[server.requests.length - 1].url).toBe(`${API_BASE_PATH}/templates`); + expect(server.requests[server.requests.length - 1].url).toBe( + `${API_BASE_PATH}/index-templates` + ); }); test('should have a button to create a new template', () => { const { exists } = testBed; - expect(exists('createTemplateButton')).toBe(true); + expect(exists('createLegacyTemplateButton')).toBe(true); }); test('should have a switch to view system templates', async () => { - const { table, exists, component, form } = testBed; - const { rows } = table.getMetaData('templateTable'); + const { table, exists, actions } = testBed; + const { rows } = table.getMetaData('legacyTemplateTable'); expect(rows.length).toEqual( - templates.filter((template) => !template.name.startsWith('.')).length + legacyTemplates.filter((template) => !template.name.startsWith('.')).length ); - expect(exists('systemTemplatesSwitch')).toBe(true); + expect(exists('viewButton')).toBe(true); - await act(async () => { - form.toggleEuiSwitch('systemTemplatesSwitch'); - await nextTick(); - component.update(); - }); + actions.toggleViewItem('system'); - const { rows: updatedRows } = table.getMetaData('templateTable'); - expect(updatedRows.length).toEqual(templates.length); + const { rows: updatedRows } = table.getMetaData('legacyTemplateTable'); + expect(updatedRows.length).toEqual(legacyTemplates.length); }); test('each row should have a link to the template details panel', async () => { @@ -176,12 +220,12 @@ describe('Index Templates tab', () => { expect(exists('templateList')).toBe(true); expect(exists('templateDetails')).toBe(true); - expect(find('templateDetails.title').text()).toBe(template1.name); + expect(find('templateDetails.title').text()).toBe(legacyTemplates[0].name); }); test('template actions column should have an option to delete', () => { const { actions, findAction } = testBed; - const { name: templateName } = template1; + const [{ name: templateName }] = legacyTemplates; actions.clickActionMenu(templateName); @@ -192,7 +236,7 @@ describe('Index Templates tab', () => { test('template actions column should have an option to clone', () => { const { actions, findAction } = testBed; - const { name: templateName } = template1; + const [{ name: templateName }] = legacyTemplates; actions.clickActionMenu(templateName); @@ -203,7 +247,7 @@ describe('Index Templates tab', () => { test('template actions column should have an option to edit', () => { const { actions, findAction } = testBed; - const { name: templateName } = template1; + const [{ name: templateName }] = legacyTemplates; actions.clickActionMenu(templateName); @@ -215,7 +259,7 @@ describe('Index Templates tab', () => { describe('delete index template', () => { test('should show a confirmation when clicking the delete template button', async () => { const { actions } = testBed; - const { name: templateName } = template1; + const [{ name: templateName }] = legacyTemplates; await actions.clickTemplateAction(templateName, 'delete'); @@ -231,32 +275,28 @@ describe('Index Templates tab', () => { }); test('should show a warning message when attempting to delete a system template', async () => { - const { component, form, actions } = testBed; + const { exists, actions } = testBed; - await act(async () => { - form.toggleEuiSwitch('systemTemplatesSwitch'); - await nextTick(); - component.update(); - }); + actions.toggleViewItem('system'); - const { name: systemTemplateName } = template3; + const { name: systemTemplateName } = legacyTemplates[2]; await actions.clickTemplateAction(systemTemplateName, 'delete'); - expect( - document.body.querySelector('[data-test-subj="deleteSystemTemplateCallOut"]') - ).not.toBe(null); + expect(exists('deleteSystemTemplateCallOut')).toBe(true); }); test('should send the correct HTTP request to delete an index template', async () => { - const { component, actions, table } = testBed; - const { rows } = table.getMetaData('templateTable'); + const { actions, table } = testBed; + const { rows } = table.getMetaData('legacyTemplateTable'); const templateId = rows[0].columns[2].value; - const { - name: templateName, - _kbnMeta: { formatVersion }, - } = template1; + const [ + { + name: templateName, + _kbnMeta: { isLegacy }, + }, + ] = legacyTemplates; await actions.clickTemplateAction(templateName, 'delete'); const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]'); @@ -273,16 +313,14 @@ describe('Index Templates tab', () => { await act(async () => { confirmButton!.click(); - await nextTick(); - component.update(); }); const latestRequest = server.requests[server.requests.length - 1]; expect(latestRequest.method).toBe('POST'); - expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-templates`); + expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-index-templates`); expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ - templates: [{ name: template1.name, formatVersion }], + templates: [{ name: legacyTemplates[0].name, isLegacy }], }); }); }); @@ -292,6 +330,7 @@ describe('Index Templates tab', () => { const template = fixtures.getTemplate({ name: `a${getRandomString()}`, indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + isLegacy: true, }); httpRequestsMockHelpers.setLoadTemplateResponse(template); @@ -316,7 +355,7 @@ describe('Index Templates tab', () => { test('should set the correct title', async () => { const { find } = testBed; - const { name } = template1; + const [{ name }] = legacyTemplates; expect(find('templateDetails.title').text()).toEqual(name); }); @@ -327,12 +366,10 @@ describe('Index Templates tab', () => { expect(exists('closeDetailsButton')).toBe(true); expect(exists('summaryTab')).toBe(true); - actions.clickCloseDetailsButton(); - await act(async () => { - await nextTick(); - component.update(); + actions.clickCloseDetailsButton(); }); + component.update(); expect(exists('summaryTab')).toBe(false); }); @@ -372,6 +409,7 @@ describe('Index Templates tab', () => { alias1: {}, }, }, + isLegacy: true, }); const { find, actions, exists } = testBed; @@ -412,19 +450,15 @@ describe('Index Templates tab', () => { const templateWithNoOptionalFields = fixtures.getTemplate({ name: `a${getRandomString()}`, indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + isLegacy: true, }); - const { actions, find, exists, component } = testBed; + const { actions, find, exists } = testBed; httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoOptionalFields); await actions.clickTemplateAt(0); - await act(async () => { - await nextTick(); - component.update(); - }); - expect(find('templateDetails.tab').length).toBe(4); expect(exists('summaryTab')).toBe(true); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index cf00e0f6d14e1..11c25ffbb590f 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -8,9 +8,17 @@ import { act } from 'react-dom/test-utils'; import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment, nextTick } from '../helpers'; - import { IndicesTestBed, setup } from './indices_tab.helpers'; +/** + * The below import is required to avoid a console error warn from the "brace" package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import { stubWebWorker } from '../../../../../test_utils/stub_web_worker'; +stubWebWorker(); + describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: IndicesTestBed; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx index e0db9cd58ee23..6250ef0dc247d 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_clone.test.tsx @@ -53,6 +53,7 @@ describe.skip('', () => { template: { mappings: MAPPINGS, }, + isLegacy: true, }); beforeEach(async () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 95545b6c66f54..50b35fc76721a 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../common'; +import { CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../common'; import { setupEnvironment, nextTick } from '../helpers'; import { @@ -344,7 +344,6 @@ describe.skip('', () => { const latestRequest = server.requests[server.requests.length - 1]; const expected = { - isManaged: false, name: TEMPLATE_NAME, indexPatterns: DEFAULT_INDEX_PATTERNS, template: { @@ -366,7 +365,8 @@ describe.skip('', () => { aliases: ALIASES, }, _kbnMeta: { - formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT, + isLegacy: CREATE_LEGACY_TEMPLATE_BY_DEFAULT, + isManaged: false, }, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index 6e935a5263301..88067d479f7e7 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -62,6 +62,7 @@ describe.skip('', () => { const templateToEdit = fixtures.getTemplate({ name: 'index_template_without_mappings', indexPatterns: ['indexPattern1'], + isLegacy: true, }); beforeEach(async () => { @@ -102,6 +103,7 @@ describe.skip('', () => { template: { mappings: MAPPING, }, + isLegacy: true, }); beforeEach(async () => { @@ -206,9 +208,9 @@ describe.skip('', () => { settings: SETTINGS, aliases: ALIASES, }, - isManaged: false, _kbnMeta: { - formatVersion: templateToEdit._kbnMeta.formatVersion, + isManaged: false, + isLegacy: templateToEdit._kbnMeta.isLegacy, }, }; diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts index 966e2e8e64838..526b9fede2a67 100644 --- a/x-pack/plugins/index_management/common/constants/index.ts +++ b/x-pack/plugins/index_management/common/constants/index.ts @@ -9,7 +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 { CREATE_LEGACY_TEMPLATE_BY_DEFAULT } 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 index 788e96ee895ed..7696b3832c51e 100644 --- a/x-pack/plugins/index_management/common/constants/index_templates.ts +++ b/x-pack/plugins/index_management/common/constants/index_templates.ts @@ -6,7 +6,7 @@ /** * 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. + * legacy and composable index template formats. This constant keeps track of whether + * we create legacy index template format by default in the UI. */ -export const DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT = 1; +export const CREATE_LEGACY_TEMPLATE_BY_DEFAULT = true; diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts index 459eda7552c85..3792e322ae40b 100644 --- a/x-pack/plugins/index_management/common/index.ts +++ b/x-pack/plugins/index_management/common/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PLUGIN, API_BASE_PATH, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from './constants'; +export { PLUGIN, API_BASE_PATH, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from './constants'; export { getTemplateParameter } from './lib'; diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index 33f7fbe45182e..16eb544c56a08 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ export { + deserializeLegacyTemplateList, deserializeTemplateList, - deserializeV1Template, - serializeV1Template, + deserializeLegacyTemplate, + serializeLegacyTemplate, } 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 33a83d1e9335b..249881f668d9f 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -5,60 +5,34 @@ */ import { TemplateDeserialized, - TemplateV1Serialized, - TemplateV2Serialized, + LegacyTemplateSerialized, + TemplateSerialized, TemplateListItem, } from '../types'; const hasEntries = (data: object = {}) => Object.entries(data).length > 0; -export function serializeV1Template(template: TemplateDeserialized): TemplateV1Serialized { - const { - name, - version, - order, - indexPatterns, - template: { settings, aliases, mappings } = {} as TemplateDeserialized['template'], - } = template; +export function serializeTemplate(templateDeserialized: TemplateDeserialized): TemplateSerialized { + const { version, priority, indexPatterns, template, composedOf } = templateDeserialized; - const serializedTemplate: TemplateV1Serialized = { - name, + return { version, - order, + priority, + template, index_patterns: indexPatterns, - settings, - aliases, - mappings, - }; - - return serializedTemplate; -} - -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, + composed_of: composedOf, }; } -export function deserializeV2Template( - templateEs: TemplateV2Serialized, +export function deserializeTemplate( + templateEs: TemplateSerialized & { name: string }, managedTemplatePrefix?: string ): TemplateDeserialized { const { name, version, - order, index_patterns: indexPatterns, - template, + template = {}, priority, composed_of: composedOf, } = templateEs; @@ -67,49 +41,92 @@ export function deserializeV2Template( const deserializedTemplate: TemplateDeserialized = { name, version, - order, + priority, indexPatterns: indexPatterns.sort(), template, - ilmPolicy: settings && settings.index && settings.index.lifecycle, - isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)), - priority, + ilmPolicy: settings?.index?.lifecycle, composedOf, _kbnMeta: { - formatVersion: 2, + isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)), }, }; return deserializedTemplate; } -export function deserializeV1Template( - templateEs: TemplateV1Serialized, +export function deserializeTemplateList( + indexTemplates: Array<{ name: string; index_template: TemplateSerialized }>, + managedTemplatePrefix?: string +): TemplateListItem[] { + return indexTemplates.map(({ name, index_template: templateSerialized }) => { + const { + template: { mappings, settings, aliases }, + ...deserializedTemplate + } = deserializeTemplate({ name, ...templateSerialized }, managedTemplatePrefix); + + return { + ...deserializedTemplate, + hasSettings: hasEntries(settings), + hasAliases: hasEntries(aliases), + hasMappings: hasEntries(mappings), + }; + }); +} + +/** + * ------------------------------------------ + * --------- LEGACY INDEX TEMPLATES --------- + * ------------------------------------------ + */ + +export function serializeLegacyTemplate(template: TemplateDeserialized): LegacyTemplateSerialized { + const { + version, + order, + indexPatterns, + template: { settings, aliases, mappings }, + } = template; + + return { + version, + order, + index_patterns: indexPatterns, + settings, + aliases, + mappings, + }; +} + +export function deserializeLegacyTemplate( + templateEs: LegacyTemplateSerialized & { name: string }, managedTemplatePrefix?: string ): TemplateDeserialized { const { settings, aliases, mappings, ...rest } = templateEs; - const deserializedTemplateV2 = deserializeV2Template( + const deserializedTemplate = deserializeTemplate( { ...rest, template: { aliases, settings, mappings } }, managedTemplatePrefix ); return { - ...deserializedTemplateV2, + ...deserializedTemplate, + order: templateEs.order, _kbnMeta: { - formatVersion: 1, + ...deserializedTemplate._kbnMeta, + isLegacy: true, }, }; } -export function deserializeTemplateList( - indexTemplatesByName: { [key: string]: Omit }, +export function deserializeLegacyTemplateList( + indexTemplatesByName: { [key: string]: LegacyTemplateSerialized }, managedTemplatePrefix?: string ): TemplateListItem[] { return Object.entries(indexTemplatesByName).map(([name, templateSerialized]) => { const { template: { mappings, settings, aliases }, ...deserializedTemplate - } = deserializeV1Template({ name, ...templateSerialized }, managedTemplatePrefix); + } = deserializeLegacyTemplate({ name, ...templateSerialized }, managedTemplatePrefix); return { ...deserializedTemplate, diff --git a/x-pack/plugins/index_management/common/lib/utils.test.ts b/x-pack/plugins/index_management/common/lib/utils.test.ts index 221d1b009cede..056101061a82b 100644 --- a/x-pack/plugins/index_management/common/lib/utils.test.ts +++ b/x-pack/plugins/index_management/common/lib/utils.test.ts @@ -3,12 +3,12 @@ * 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'; +import { LegacyTemplateSerialized, TemplateSerialized } from '../types'; +import { isLegacyTemplate } from './utils'; describe('utils', () => { - describe('getTemplateVersion', () => { - test('should detect v1 template', () => { + describe('isLegacyTemplate', () => { + test('should detect legacy template', () => { const template = { name: 'my_template', index_patterns: ['logs*'], @@ -16,10 +16,10 @@ describe('utils', () => { properties: {}, }, }; - expect(getTemplateVersion(template as TemplateV1Serialized)).toBe(1); + expect(isLegacyTemplate(template as LegacyTemplateSerialized)).toBe(true); }); - test('should detect v2 template', () => { + test('should detect composable template', () => { const template = { name: 'my_template', index_patterns: ['logs*'], @@ -29,7 +29,7 @@ describe('utils', () => { }, }, }; - expect(getTemplateVersion(template as TemplateV2Serialized)).toBe(2); + expect(isLegacyTemplate(template as TemplateSerialized)).toBe(false); }); }); }); diff --git a/x-pack/plugins/index_management/common/lib/utils.ts b/x-pack/plugins/index_management/common/lib/utils.ts index eee35dc1ab467..5a7db8ef50ab4 100644 --- a/x-pack/plugins/index_management/common/lib/utils.ts +++ b/x-pack/plugins/index_management/common/lib/utils.ts @@ -4,26 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TemplateDeserialized, TemplateV1Serialized, TemplateV2Serialized } from '../types'; +import { TemplateDeserialized, LegacyTemplateSerialized, TemplateSerialized } 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 + * Helper to know if a template has the legacy format or not + * legacy format will be supported up until 9.x but marked as deprecated from 7.8 + * new (composable) format is supported from 7.8 */ -export const getTemplateVersion = ( - template: TemplateDeserialized | TemplateV1Serialized | TemplateV2Serialized -): 1 | 2 => { - return {}.hasOwnProperty.call(template, 'template') ? 2 : 1; +export const isLegacyTemplate = ( + template: TemplateDeserialized | LegacyTemplateSerialized | TemplateSerialized +): boolean => { + return {}.hasOwnProperty.call(template, 'template') ? false : true; }; export const getTemplateParameter = ( - template: TemplateV1Serialized | TemplateV2Serialized, + template: LegacyTemplateSerialized | TemplateSerialized, setting: 'aliases' | 'settings' | 'mappings' ) => { - const formatVersion = getTemplateVersion(template); - - return formatVersion === 1 - ? (template as TemplateV1Serialized)[setting] - : (template as TemplateV2Serialized).template[setting]; + return isLegacyTemplate(template) + ? (template as LegacyTemplateSerialized)[setting] + : (template as TemplateSerialized).template[setting]; }; diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index c37088982f207..f113aa44d058f 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -8,28 +8,45 @@ import { IndexSettings } from './indices'; import { Aliases } from './aliases'; import { Mappings } from './mappings'; -// Template serialized (from Elasticsearch) -interface TemplateBaseSerialized { - name: string; +/** + * Index template format from Elasticsearch + */ +export interface TemplateSerialized { index_patterns: string[]; + template: { + settings?: IndexSettings; + aliases?: Aliases; + mappings?: Mappings; + }; + composed_of?: string[]; version?: number; - order?: number; -} - -export interface TemplateV1Serialized extends TemplateBaseSerialized { - settings?: IndexSettings; - aliases?: Aliases; - mappings?: Mappings; + priority?: number; } -export interface TemplateV2Serialized extends TemplateBaseSerialized { +/** + * TemplateDeserialized is the format the UI will be working with, + * regardless if we are loading the new format (composable) index template, + * or the legacy one. Serialization is done server side. + */ +export interface TemplateDeserialized { + name: string; + indexPatterns: string[]; template: { settings?: IndexSettings; aliases?: Aliases; mappings?: Mappings; }; + composedOf?: string[]; // Used on composable index template + version?: number; priority?: number; - composed_of?: string[]; + order?: number; // Used on legacy index template + ilmPolicy?: { + name: string; + }; + _kbnMeta: { + isManaged: boolean; + isLegacy?: boolean; + }; } /** @@ -42,42 +59,30 @@ export interface TemplateListItem { indexPatterns: string[]; version?: number; order?: number; + priority?: number; hasSettings: boolean; hasAliases: boolean; hasMappings: boolean; ilmPolicy?: { name: string; }; - isManaged: boolean; _kbnMeta: { - formatVersion: IndexTemplateFormatVersion; + isManaged: boolean; + isLegacy?: boolean; }; } /** - * 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. + * ------------------------------------------ + * --------- LEGACY INDEX TEMPLATES --------- + * ------------------------------------------ */ -export interface TemplateDeserialized { - name: string; - indexPatterns: string[]; - isManaged: boolean; - template: { - settings?: IndexSettings; - aliases?: Aliases; - mappings?: Mappings; - }; - _kbnMeta: { - formatVersion: IndexTemplateFormatVersion; - }; + +export interface LegacyTemplateSerialized { + index_patterns: string[]; version?: number; - priority?: number; + settings?: IndexSettings; + aliases?: Aliases; + mappings?: Mappings; order?: number; - ilmPolicy?: { - name: string; - }; - composedOf?: string[]; } - -export type IndexTemplateFormatVersion = 1 | 2; 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 a87412ef92950..06babb0db3bd1 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 @@ -9,7 +9,6 @@ import { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox, EuiBadge } fr import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexTemplateFormatVersion } from '../../../common'; import { deleteTemplates } from '../services/api'; import { notificationService } from '../services/notification'; @@ -17,7 +16,7 @@ export const TemplateDeleteModal = ({ templatesToDelete, callback, }: { - templatesToDelete: Array<{ name: string; formatVersion: IndexTemplateFormatVersion }>; + templatesToDelete: Array<{ name: string; isLegacy?: boolean }>; callback: (data?: { hasDeletedTemplates: boolean }) => void; }) => { const [isDeleteConfirmed, setIsDeleteConfirmed] = useState(false); 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 7b266034bc336..387887239aaf0 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 @@ -23,8 +23,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { serializers } from '../../../../shared_imports'; import { - serializeV1Template, - serializeV2Template, + serializeLegacyTemplate, + serializeTemplate, } from '../../../../../common/lib/template_serialization'; import { TemplateDeserialized, getTemplateParameter } from '../../../../../common'; import { StepProps } from '../types'; @@ -60,16 +60,20 @@ export const StepReview: React.FunctionComponent = ({ template, updat indexPatterns, version, order, - _kbnMeta: { formatVersion }, + _kbnMeta: { isLegacy }, } = template!; - const serializedTemplate = - formatVersion === 1 - ? serializeV1Template(stripEmptyFields(template!) as TemplateDeserialized) - : serializeV2Template(stripEmptyFields(template!) as TemplateDeserialized); - - // Name not included in ES request body - delete serializedTemplate.name; + const serializedTemplate = isLegacy + ? serializeLegacyTemplate( + stripEmptyFields(template!, { + types: ['string'], + }) as TemplateDeserialized + ) + : serializeTemplate( + stripEmptyFields(template!, { + types: ['string'], + }) as TemplateDeserialized + ); const serializedMappings = getTemplateParameter(serializedTemplate, 'mappings'); const serializedSettings = getTemplateParameter(serializedTemplate, 'settings'); 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 0cdfaae70f151..52e26e6d3e895 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 { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../../../common'; +import { TemplateDeserialized, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../../common'; import { TemplateSteps } from './template_steps'; import { StepAliases, StepLogistics, StepMappings, StepSettings, StepReview } from './steps'; import { StepProps, DataGetterFunc } from './types'; @@ -51,9 +51,9 @@ export const TemplateForm: React.FunctionComponent = ({ name: '', indexPatterns: [], template: {}, - isManaged: false, _kbnMeta: { - formatVersion: DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT, + isManaged: false, + isLegacy: CREATE_LEGACY_TEMPLATE_BY_DEFAULT, }, }, onSave, @@ -246,7 +246,9 @@ export const TemplateForm: React.FunctionComponent = ({ iconType="check" onClick={onSave.bind( null, - stripEmptyFields(template.current!) as TemplateDeserialized + stripEmptyFields(template.current!, { + types: ['string'], + }) as TemplateDeserialized )} data-test-subj="submitButton" isLoading={isSaving} 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 index 7129e536287c1..08102ae93cc0e 100644 --- a/x-pack/plugins/index_management/public/application/lib/index_templates.ts +++ b/x-pack/plugins/index_management/public/application/lib/index_templates.ts @@ -6,12 +6,12 @@ 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)); +export const getIsLegacyFromQueryParams = (location: Location): boolean => { + const { legacy } = parse(location.search.substring(1)); - if (!Boolean(version) || typeof version !== 'string') { - return undefined; + if (!Boolean(legacy) || typeof legacy !== 'string') { + return false; } - return +version as 1 | 2; + return legacy === 'true'; }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/filter_list_button.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/filter_list_button.tsx new file mode 100644 index 0000000000000..1c95cca3fead6 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/filter_list_button.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFilterButton, EuiPopover, EuiFilterSelectItem } from '@elastic/eui'; + +interface Filter { + name: string; + checked: 'on' | 'off'; +} + +interface Props { + filters: Filters; + onChange(filters: Filters): void; +} + +export type Filters = { + [key in T]: Filter; +}; + +export function FilterListButton({ onChange, filters }: Props) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const activeFilters = Object.values(filters).filter((v) => (v as Filter).checked === 'on'); + + const onButtonClick = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const toggleFilter = (filter: T) => { + const previousValue = filters[filter].checked; + onChange({ + ...filters, + [filter]: { + ...filters[filter], + checked: previousValue === 'on' ? 'off' : 'on', + }, + }); + }; + + const button = ( + 0} + numActiveFilters={activeFilters.length} + data-test-subj="viewButton" + > + + + ); + + return ( + +
+ {Object.entries(filters).map(([filter, item], index) => ( + toggleFilter(filter as T)} + data-test-subj="filterItem" + > + {(item as Filter).name} + + ))} +
+
+ ); +} diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts new file mode 100644 index 0000000000000..dcaba319bb21a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './filter_list_button'; +export * from './template_content_indicator'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx new file mode 100644 index 0000000000000..78e33d7940bd4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_content_indicator.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; + +interface Props { + mappings: boolean; + settings: boolean; + aliases: boolean; +} + +const texts = { + settings: i18n.translate('xpack.idxMgmt.templateContentIndicator.indexSettingsTooltipLabel', { + defaultMessage: 'Index settings', + }), + mappings: i18n.translate('xpack.idxMgmt.templateContentIndicator.mappingsTooltipLabel', { + defaultMessage: 'Mappings', + }), + aliases: i18n.translate('xpack.idxMgmt.templateContentIndicator.aliasesTooltipLabel', { + defaultMessage: 'Aliases', + }), +}; + +export const TemplateContentIndicator = ({ mappings, settings, aliases }: Props) => { + const getColor = (flag: boolean) => (flag ? 'primary' : 'hollow'); + + return ( + <> + + <> + M +   + + + + <> + S +   + + + + A + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts new file mode 100644 index 0000000000000..519120b559e7b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LegacyTemplateDetails } from './template_details'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx new file mode 100644 index 0000000000000..ec2956973d4f6 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx @@ -0,0 +1,327 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiCallOut, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiTab, + EuiTabs, + EuiSpacer, + EuiPopover, + EuiButton, + EuiContextMenu, +} from '@elastic/eui'; +import { + UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +} from '../../../../../../../common/constants'; +import { TemplateDeserialized } from '../../../../../../../common'; +import { + TemplateDeleteModal, + SectionLoading, + SectionError, + Error, +} from '../../../../../components'; +import { useLoadIndexTemplate } from '../../../../../services/api'; +import { decodePath } from '../../../../../services/routing'; +import { SendRequestResponse } from '../../../../../../shared_imports'; +import { useServices } from '../../../../../app_context'; +import { TabSummary, TabMappings, TabSettings, TabAliases } from '../../template_details/tabs'; + +interface Props { + template: { name: string; isLegacy?: boolean }; + onClose: () => void; + editTemplate: (name: string, isLegacy?: boolean) => void; + cloneTemplate: (name: string, isLegacy?: boolean) => void; + reload: () => Promise; +} + +const SUMMARY_TAB_ID = 'summary'; +const MAPPINGS_TAB_ID = 'mappings'; +const ALIASES_TAB_ID = 'aliases'; +const SETTINGS_TAB_ID = 'settings'; + +const TABS = [ + { + id: SUMMARY_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.summaryTabTitle', { + defaultMessage: 'Summary', + }), + }, + { + id: SETTINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.settingsTabTitle', { + defaultMessage: 'Settings', + }), + }, + { + id: MAPPINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.mappingsTabTitle', { + defaultMessage: 'Mappings', + }), + }, + { + id: ALIASES_TAB_ID, + name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.aliasesTabTitle', { + defaultMessage: 'Aliases', + }), + }, +]; + +const tabToComponentMap: { + [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>; +} = { + [SUMMARY_TAB_ID]: TabSummary, + [SETTINGS_TAB_ID]: TabSettings, + [MAPPINGS_TAB_ID]: TabMappings, + [ALIASES_TAB_ID]: TabAliases, +}; + +const tabToUiMetricMap: { [key: string]: string } = { + [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +}; + +export const LegacyTemplateDetails: React.FunctionComponent = ({ + template: { name: templateName, isLegacy }, + onClose, + editTemplate, + cloneTemplate, + reload, +}) => { + const { uiMetricService } = useServices(); + const decodedTemplateName = decodePath(templateName); + const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( + decodedTemplateName, + isLegacy + ); + const isManaged = templateDetails?._kbnMeta.isManaged ?? false; + const [templateToDelete, setTemplateToDelete] = useState< + Array<{ name: string; isLegacy?: boolean }> + >([]); + const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); + const [isPopoverOpen, setIsPopOverOpen] = useState(false); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error as Error} + data-test-subj="sectionError" + /> + ); + } else if (templateDetails) { + const Content = tabToComponentMap[activeTab]; + const managedTemplateCallout = isManaged ? ( + + + } + color="primary" + size="s" + > + + + + + ) : null; + + content = ( + + {managedTemplateCallout} + + + {TABS.map((tab) => ( + { + uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); + setActiveTab(tab.id); + }} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subj="tab" + > + {tab.name} + + ))} + + + + + + + ); + } + + return ( + + {templateToDelete && templateToDelete.length > 0 ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } else { + setTemplateToDelete([]); + } + onClose(); + }} + templatesToDelete={templateToDelete} + /> + ) : null} + + + + +

+ {decodedTemplateName} +

+
+
+ + {content} + + + + + + + + + {templateDetails && ( + + {/* Manage templates context menu */} + setIsPopOverOpen((prev) => !prev)} + > + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopOverOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="rightUp" + repositionOnScroll + > + editTemplate(templateName, isLegacy), + disabled: isManaged, + }, + { + name: i18n.translate( + 'xpack.idxMgmt.legacyTemplateDetails.cloneButtonLabel', + { + defaultMessage: 'Clone', + } + ), + icon: 'copy', + onClick: () => cloneTemplate(templateName, isLegacy), + }, + { + name: i18n.translate( + 'xpack.idxMgmt.legacyTemplateDetails.deleteButtonLabel', + { + defaultMessage: 'Delete', + } + ), + icon: 'trash', + onClick: () => + setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]), + disabled: isManaged, + }, + ], + }, + ]} + /> + + + )} + + +
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/index.ts new file mode 100644 index 0000000000000..a8499df45ce27 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LegacyTemplateTable } from './template_table'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx new file mode 100644 index 0000000000000..92fedd5d68f00 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -0,0 +1,304 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; +import { reactRouterNavigate } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { TemplateListItem } from '../../../../../../../common'; +import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/constants'; +import { TemplateDeleteModal } from '../../../../../components'; +import { useServices } from '../../../../../app_context'; +import { SendRequestResponse } from '../../../../../../shared_imports'; + +interface Props { + templates: TemplateListItem[]; + reload: () => Promise; + editTemplate: (name: string, isLegacy?: boolean) => void; + cloneTemplate: (name: string, isLegacy?: boolean) => void; + history: ScopedHistory; +} + +export const LegacyTemplateTable: React.FunctionComponent = ({ + templates, + reload, + editTemplate, + cloneTemplate, + history, +}) => { + const { uiMetricService } = useServices(); + const [selection, setSelection] = useState([]); + const [templatesToDelete, setTemplatesToDelete] = useState< + Array<{ name: string; isLegacy?: boolean }> + >([]); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.nameColumnTitle', { + defaultMessage: 'Name', + }), + truncateText: true, + sortable: true, + 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) + )} + data-test-subj="templateDetailsLink" + > + {name} + + ); + }, + }, + { + field: 'indexPatterns', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.indexPatternsColumnTitle', { + defaultMessage: 'Index patterns', + }), + truncateText: true, + sortable: true, + render: (indexPatterns: string[]) => {indexPatterns.join(', ')}, + }, + { + field: 'ilmPolicy', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.ilmPolicyColumnTitle', { + defaultMessage: 'ILM policy', + }), + truncateText: true, + sortable: true, + render: (ilmPolicy: { name: string }) => + ilmPolicy && ilmPolicy.name ? ( + + {ilmPolicy.name} + + ) : null, + }, + { + field: 'order', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.orderColumnTitle', { + defaultMessage: 'Order', + }), + truncateText: true, + sortable: true, + }, + { + field: 'hasMappings', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.mappingsColumnTitle', { + defaultMessage: 'Mappings', + }), + truncateText: true, + sortable: true, + render: (hasMappings: boolean) => (hasMappings ? : null), + }, + { + field: 'hasSettings', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.settingsColumnTitle', { + defaultMessage: 'Settings', + }), + truncateText: true, + sortable: true, + render: (hasSettings: boolean) => (hasSettings ? : null), + }, + { + field: 'hasAliases', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.aliasesColumnTitle', { + defaultMessage: 'Aliases', + }), + truncateText: true, + sortable: true, + render: (hasAliases: boolean) => (hasAliases ? : null), + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionEditText', { + defaultMessage: 'Edit', + }), + isPrimary: true, + description: i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.actionEditDecription', + { + defaultMessage: 'Edit this template', + } + ), + icon: 'pencil', + type: 'icon', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + editTemplate(name, isLegacy); + }, + enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + }, + { + type: 'icon', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionCloneTitle', { + defaultMessage: 'Clone', + }), + description: i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.actionCloneDescription', + { + defaultMessage: 'Clone this template', + } + ), + icon: 'copy', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + cloneTemplate(name, isLegacy); + }, + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionDeleteText', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.actionDeleteDecription', + { + defaultMessage: 'Delete this template', + } + ), + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + setTemplatesToDelete([{ name, isLegacy }]); + }, + isPrimary: true, + enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + }, + ], + }, + ]; + + const pagination = { + initialPageSize: 20, + pageSizeOptions: [10, 20, 50], + }; + + const sorting = { + sort: { + field: 'name', + direction: 'asc', + }, + } as const; + + const selectionConfig = { + onSelectionChange: setSelection, + selectable: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + selectableMessage: (selectable: boolean) => { + if (!selectable) { + return i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip', + { + defaultMessage: 'You cannot delete a managed template.', + } + ); + } + return ''; + }, + }; + + const searchConfig = { + box: { + incremental: true, + }, + toolsLeft: + selection.length > 0 ? ( + + setTemplatesToDelete( + selection.map(({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => ({ + name, + isLegacy, + })) + ) + } + color="danger" + > + + + ) : undefined, + toolsRight: [ + + + , + ], + }; + + return ( + + {templatesToDelete && templatesToDelete.length > 0 ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } else { + setTemplatesToDelete([]); + } + }} + templatesToDelete={templatesToDelete} + /> + ) : null} + ({ + 'data-test-subj': 'row', + })} + cellProps={() => ({ + 'data-test-subj': 'cell', + })} + data-test-subj="legacyTemplateTable" + message={ + + } + /> + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx index ed403276af564..9f51f114176fb 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx @@ -4,313 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiTab, - EuiTabs, - EuiSpacer, - EuiPopover, - EuiButton, - EuiContextMenu, -} from '@elastic/eui'; -import { - UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -} from '../../../../../../common/constants'; -import { TemplateDeserialized, IndexTemplateFormatVersion } from '../../../../../../common'; -import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; -import { useLoadIndexTemplate } from '../../../../services/api'; -import { decodePath } from '../../../../services/routing'; -import { SendRequestResponse } from '../../../../../shared_imports'; -import { useServices } from '../../../../app_context'; -import { TabSummary, TabMappings, TabSettings, TabAliases } from './tabs'; +import React from 'react'; -interface Props { - template: { name: string; formatVersion: IndexTemplateFormatVersion }; - onClose: () => void; - editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - reload: () => Promise; -} - -const SUMMARY_TAB_ID = 'summary'; -const MAPPINGS_TAB_ID = 'mappings'; -const ALIASES_TAB_ID = 'aliases'; -const SETTINGS_TAB_ID = 'settings'; - -const TABS = [ - { - id: SUMMARY_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.summaryTabTitle', { - defaultMessage: 'Summary', - }), - }, - { - id: SETTINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.settingsTabTitle', { - defaultMessage: 'Settings', - }), - }, - { - id: MAPPINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.mappingsTabTitle', { - defaultMessage: 'Mappings', - }), - }, - { - id: ALIASES_TAB_ID, - name: i18n.translate('xpack.idxMgmt.templateDetails.aliasesTabTitle', { - defaultMessage: 'Aliases', - }), - }, -]; - -const tabToComponentMap: { - [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>; -} = { - [SUMMARY_TAB_ID]: TabSummary, - [SETTINGS_TAB_ID]: TabSettings, - [MAPPINGS_TAB_ID]: TabMappings, - [ALIASES_TAB_ID]: TabAliases, -}; - -const tabToUiMetricMap: { [key: string]: string } = { - [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -}; - -export const TemplateDetails: React.FunctionComponent = ({ - template: { name: templateName, formatVersion }, - onClose, - editTemplate, - cloneTemplate, - reload, -}) => { - const { uiMetricService } = useServices(); - const decodedTemplateName = decodePath(templateName); - 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); - - let content; - - if (isLoading) { - content = ( - - - - ); - } else if (error) { - content = ( - - } - error={error as Error} - data-test-subj="sectionError" - /> - ); - } else if (templateDetails) { - const Content = tabToComponentMap[activeTab]; - const managedTemplateCallout = isManaged ? ( - - - } - color="primary" - size="s" - > - - - - - ) : null; - - content = ( - - {managedTemplateCallout} - - - {TABS.map((tab) => ( - { - uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); - setActiveTab(tab.id); - }} - isSelected={tab.id === activeTab} - key={tab.id} - data-test-subj="tab" - > - {tab.name} - - ))} - - - - - - - ); - } - - return ( - - {templateToDelete && templateToDelete.length > 0 ? ( - { - if (data && data.hasDeletedTemplates) { - reload(); - } else { - setTemplateToDelete([]); - } - onClose(); - }} - templatesToDelete={templateToDelete} - /> - ) : null} - - - - -

- {decodedTemplateName} -

-
-
- - {content} - - - - - - - - - {templateDetails && ( - - {/* Manage templates context menu */} - setIsPopOverOpen((prev) => !prev)} - > - - - } - isOpen={isPopoverOpen} - closePopover={() => setIsPopOverOpen(false)} - panelPaddingSize="none" - withTitle - anchorPosition="rightUp" - repositionOnScroll - > - editTemplate(templateName, formatVersion), - disabled: isManaged, - }, - { - name: i18n.translate('xpack.idxMgmt.templateDetails.cloneButtonLabel', { - defaultMessage: 'Clone', - }), - icon: 'copy', - onClick: () => cloneTemplate(templateName, formatVersion), - }, - { - name: i18n.translate( - 'xpack.idxMgmt.templateDetails.deleteButtonLabel', - { - defaultMessage: 'Delete', - } - ), - icon: 'trash', - onClick: () => - setTemplateToDelete([{ name: decodedTemplateName, formatVersion }]), - disabled: isManaged, - }, - ], - }, - ]} - /> - - - )} - - -
-
- ); +export const TemplateDetails: React.FunctionComponent = () => { + // TODO new (V2) templatte details + return null; }; 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 db0833ea03233..fc3d5125e3066 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 @@ -7,19 +7,20 @@ import React, { Fragment, useState, useEffect, useMemo } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { ScopedHistory } from 'kibana/public'; import { EuiEmptyPrompt, EuiSpacer, EuiTitle, EuiText, - EuiSwitch, EuiFlexItem, EuiFlexGroup, + EuiButton, } from '@elastic/eui'; import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants'; -import { IndexTemplateFormatVersion } from '../../../../../common'; +import { TemplateListItem } from '../../../../../common'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadIndexTemplates } from '../../../services/api'; import { useServices } from '../../../app_context'; @@ -28,14 +29,20 @@ import { getTemplateListLink, getTemplateCloneLink, } from '../../../services/routing'; -import { getFormatVersionFromQueryparams } from '../../../lib/index_templates'; +import { getIsLegacyFromQueryParams } from '../../../lib/index_templates'; import { TemplateTable } from './template_table'; -import { TemplateDetails } from './template_details'; +import { LegacyTemplateTable } from './legacy_templates/template_table'; +import { LegacyTemplateDetails } from './legacy_templates/template_details'; +import { FilterListButton, Filters } from './components'; +type FilterName = 'composable' | 'system'; interface MatchParams { templateName?: string; } +const stripOutSystemTemplates = (templates: TemplateListItem[]): TemplateListItem[] => + templates.filter((template) => !template.name.startsWith('.')); + export const TemplateList: React.FunctionComponent> = ({ match: { params: { templateName }, @@ -44,122 +51,188 @@ export const TemplateList: React.FunctionComponent { const { uiMetricService } = useServices(); - const { error, isLoading, data: templates, sendRequest: reload } = useLoadIndexTemplates(); - const queryParamsFormatVersion = getFormatVersionFromQueryparams(location); + const { error, isLoading, data: allTemplates, sendRequest: reload } = useLoadIndexTemplates(); - let content; + const [filters, setFilters] = useState>({ + composable: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewComposableTemplateLabel', { + defaultMessage: 'Composable templates', + }), + checked: 'on', + }, + system: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewSystemTemplateLabel', { + defaultMessage: 'System templates', + }), + checked: 'off', + }, + }); - const [showSystemTemplates, setShowSystemTemplates] = useState(false); + const filteredTemplates = useMemo(() => { + if (!allTemplates) { + return { templates: [], legacyTemplates: [] }; + } - // Filter out system index templates - const filteredTemplates = useMemo( - () => (templates ? templates.filter((template) => !template.name.startsWith('.')) : []), - [templates] - ); + return filters.system.checked === 'on' + ? allTemplates + : { + templates: stripOutSystemTemplates(allTemplates.templates), + legacyTemplates: stripOutSystemTemplates(allTemplates.legacyTemplates), + }; + }, [allTemplates, filters.system.checked]); + + const showComposableTemplateTable = filters.composable.checked === 'on'; + + const selectedTemplate = Boolean(templateName) + ? { + name: templateName!, + isLegacy: getIsLegacyFromQueryParams(location), + } + : null; + + const isLegacyTemplateDetailsVisible = selectedTemplate !== null && selectedTemplate.isLegacy; + const hasTemplates = + allTemplates && (allTemplates.legacyTemplates.length > 0 || allTemplates.templates.length > 0); const closeTemplateDetails = () => { history.push(getTemplateListLink()); }; - const editTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => { - history.push(getTemplateEditLink(name, formatVersion)); + const editTemplate = (name: string, isLegacy?: boolean) => { + history.push(getTemplateEditLink(name, isLegacy)); }; - const cloneTemplate = (name: string, formatVersion: IndexTemplateFormatVersion) => { - history.push(getTemplateCloneLink(name, formatVersion)); + const cloneTemplate = (name: string, isLegacy?: boolean) => { + history.push(getTemplateCloneLink(name, isLegacy)); }; - // Track component loaded - useEffect(() => { - uiMetricService.trackMetric('loaded', UIM_TEMPLATE_LIST_LOAD); - }, [uiMetricService]); + const renderHeader = () => ( + + + + + + + + + + filters={filters} onChange={setFilters} /> + + + + + + + + ); - if (isLoading) { - content = ( - - - - ); - } else if (error) { - content = ( - + showComposableTemplateTable ? ( + <> + + + + ) : null; + + const renderLegacyTemplatesTable = () => ( + <> + + +

- } - error={error as Error} +

+
+ + - ); - } else if (Array.isArray(templates) && templates.length === 0) { - content = ( - + + ); + + const renderContent = () => { + if (isLoading) { + return ( + + + + ); + } else if (error) { + return ( + - - } - data-test-subj="emptyPrompt" - /> - ); - } else if (Array.isArray(templates) && templates.length > 0) { - content = ( - - - - - - - - - - - setShowSystemTemplates(event.target.checked)} - label={ - - } - /> - - - - + ); + } else if (!hasTemplates) { + return ( + + + + } + data-test-subj="emptyPrompt" /> - - ); - } + ); + } else { + return ( + + {/* Header */} + {renderHeader()} + + {/* Composable index templates table */} + {renderTemplatesTable()} + + {/* Legacy index templates table */} + {renderLegacyTemplatesTable()} + + ); + } + }; + + // Track component loaded + useEffect(() => { + uiMetricService.trackMetric('loaded', UIM_TEMPLATE_LIST_LOAD); + }, [uiMetricService]); return (
- {content} - {templateName && queryParamsFormatVersion !== undefined && ( - Promise; - editTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - cloneTemplate: (name: string, formatVersion: IndexTemplateFormatVersion) => void; - history: ScopedHistory; } -export const TemplateTable: React.FunctionComponent = ({ - templates, - reload, - editTemplate, - cloneTemplate, - history, -}) => { - const { uiMetricService } = useServices(); - const [selection, setSelection] = useState([]); +export const TemplateTable: React.FunctionComponent = ({ templates, reload }) => { const [templatesToDelete, setTemplatesToDelete] = useState< - Array<{ name: string; formatVersion: IndexTemplateFormatVersion }> + Array<{ name: string; isLegacy?: boolean }> >([]); const columns: Array> = [ @@ -45,24 +31,6 @@ export const TemplateTable: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - 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) - )} - data-test-subj="templateDetailsLink" - > - {name} - - ); - }, }, { field: 'indexPatterns', @@ -95,90 +63,36 @@ export const TemplateTable: React.FunctionComponent = ({ ) : null, }, { - field: 'order', - name: i18n.translate('xpack.idxMgmt.templateList.table.orderColumnTitle', { - defaultMessage: 'Order', + field: 'composedOf', + name: i18n.translate('xpack.idxMgmt.templateList.table.componentsColumnTitle', { + defaultMessage: 'Components', }), truncateText: true, sortable: true, + render: (composedOf: string[] = []) => {composedOf.join(', ')}, }, { - field: 'hasMappings', - name: i18n.translate('xpack.idxMgmt.templateList.table.mappingsColumnTitle', { - defaultMessage: 'Mappings', + field: 'priority', + name: i18n.translate('xpack.idxMgmt.templateList.table.priorityColumnTitle', { + defaultMessage: 'Priority', }), truncateText: true, sortable: true, - render: (hasMappings: boolean) => (hasMappings ? : null), }, { - field: 'hasSettings', - name: i18n.translate('xpack.idxMgmt.templateList.table.settingsColumnTitle', { - defaultMessage: 'Settings', - }), - truncateText: true, - sortable: true, - render: (hasSettings: boolean) => (hasSettings ? : null), - }, - { - field: 'hasAliases', - name: i18n.translate('xpack.idxMgmt.templateList.table.aliasesColumnTitle', { - defaultMessage: 'Aliases', + field: 'hasMappings', + name: i18n.translate('xpack.idxMgmt.templateList.table.overridesColumnTitle', { + defaultMessage: 'Overrides', }), truncateText: true, - sortable: true, - render: (hasAliases: boolean) => (hasAliases ? : null), - }, - { - name: i18n.translate('xpack.idxMgmt.templateList.table.actionColumnTitle', { - defaultMessage: 'Actions', - }), - actions: [ - { - name: i18n.translate('xpack.idxMgmt.templateList.table.actionEditText', { - defaultMessage: 'Edit', - }), - isPrimary: true, - description: i18n.translate('xpack.idxMgmt.templateList.table.actionEditDecription', { - defaultMessage: 'Edit this template', - }), - icon: 'pencil', - type: 'icon', - onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => { - editTemplate(name, formatVersion); - }, - enabled: ({ isManaged }: TemplateListItem) => !isManaged, - }, - { - type: 'icon', - name: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneTitle', { - defaultMessage: 'Clone', - }), - description: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneDescription', { - defaultMessage: 'Clone this template', - }), - icon: 'copy', - onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => { - cloneTemplate(name, formatVersion); - }, - }, - { - name: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteText', { - defaultMessage: 'Delete', - }), - description: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteDecription', { - defaultMessage: 'Delete this template', - }), - icon: 'trash', - color: 'danger', - type: 'icon', - onClick: ({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => { - setTemplatesToDelete([{ name, formatVersion }]); - }, - isPrimary: true, - enabled: ({ isManaged }: TemplateListItem) => !isManaged, - }, - ], + sortable: false, + render: (_, item) => ( + + ), }, ]; @@ -194,70 +108,10 @@ export const TemplateTable: React.FunctionComponent = ({ }, } as const; - const selectionConfig = { - onSelectionChange: setSelection, - selectable: ({ isManaged }: TemplateListItem) => !isManaged, - selectableMessage: (selectable: boolean) => { - if (!selectable) { - return i18n.translate('xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip', { - defaultMessage: 'You cannot delete a managed template.', - }); - } - return ''; - }, - }; - const searchConfig = { box: { incremental: true, }, - toolsLeft: - selection.length > 0 ? ( - - setTemplatesToDelete( - selection.map(({ name, _kbnMeta: { formatVersion } }: TemplateListItem) => ({ - name, - formatVersion, - })) - ) - } - color="danger" - > - - - ) : undefined, - toolsRight: [ - - - , - - - , - ], }; return ( @@ -280,8 +134,7 @@ export const TemplateTable: React.FunctionComponent = ({ columns={columns} search={searchConfig} sorting={sorting} - isSelectable={true} - selection={selectionConfig} + isSelectable={false} pagination={pagination} rowProps={() => ({ 'data-test-subj': 'row', 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 b69e441feb176..8bdd230f89952 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -8,12 +8,12 @@ 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 { TemplateDeserialized } from '../../../../common'; import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; import { decodePath, getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; -import { getFormatVersionFromQueryparams } from '../../lib/index_templates'; +import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; interface MatchParams { name: string; @@ -27,14 +27,13 @@ export const TemplateClone: React.FunctionComponent { const decodedTemplateName = decodePath(name); - const formatVersion = - getFormatVersionFromQueryparams(location) ?? DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT; + const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); const { error: templateToCloneError, data: templateToClone, isLoading } = useLoadIndexTemplate( decodedTemplateName, - formatVersion + isLegacy ); const onSave = async (template: TemplateDeserialized) => { @@ -52,7 +51,7 @@ export const TemplateClone: React.FunctionComponent { diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index 27341685f3dc0..f567b9835d53d 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -33,7 +33,7 @@ export const TemplateCreate: React.FunctionComponent = ({ h return; } - history.push(getTemplateDetailsLink(name, template._kbnMeta.formatVersion)); + history.push(getTemplateDetailsLink(name, template._kbnMeta.isLegacy)); }; 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 9ad26d0af802d..d3e539989bc96 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 @@ -8,12 +8,12 @@ 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 { TemplateDeserialized } 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 { getFormatVersionFromQueryparams } from '../../lib/index_templates'; +import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; interface MatchParams { name: string; @@ -27,16 +27,12 @@ export const TemplateEdit: React.FunctionComponent { const decodedTemplateName = decodePath(name); - const formatVersion = - getFormatVersionFromQueryparams(location) ?? DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT; + const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); - const { error, data: template, isLoading } = useLoadIndexTemplate( - decodedTemplateName, - formatVersion - ); + const { error, data: template, isLoading } = useLoadIndexTemplate(decodedTemplateName, isLegacy); useEffect(() => { breadcrumbService.setBreadcrumbs('templateEdit'); @@ -55,7 +51,7 @@ export const TemplateEdit: React.FunctionComponent { @@ -87,7 +83,10 @@ export const TemplateEdit: React.FunctionComponent ); } else if (template) { - const { name: templateName, isManaged } = template; + const { + name: templateName, + _kbnMeta: { isManaged }, + } = template; const isSystemTemplate = templateName && templateName.startsWith('.'); if (isManaged) { 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 181707b3661b4..3961942b83ea3 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -37,11 +37,7 @@ import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants'; import { useRequest, sendRequest } from './use_request'; import { httpService } from './http'; import { UiMetricService } from './ui_metric'; -import { - TemplateDeserialized, - TemplateListItem, - IndexTemplateFormatVersion, -} from '../../../common'; +import { TemplateDeserialized, TemplateListItem } from '../../../common'; import { IndexMgmtMetricsType } from '../../types'; // Temporary hack to provide the uiMetricService instance to this file. @@ -214,17 +210,15 @@ export async function loadIndexData(type: string, indexName: string) { } export function useLoadIndexTemplates() { - return useRequest({ - path: `${API_BASE_PATH}/templates`, + return useRequest<{ templates: TemplateListItem[]; legacyTemplates: TemplateListItem[] }>({ + path: `${API_BASE_PATH}/index-templates`, method: 'get', }); } -export async function deleteTemplates( - templates: Array<{ name: string; formatVersion: IndexTemplateFormatVersion }> -) { +export async function deleteTemplates(templates: Array<{ name: string; isLegacy?: boolean }>) { const result = sendRequest({ - path: `${API_BASE_PATH}/delete-templates`, + path: `${API_BASE_PATH}/delete-index-templates`, method: 'post', body: { templates }, }); @@ -236,23 +230,20 @@ export async function deleteTemplates( return result; } -export function useLoadIndexTemplate( - name: TemplateDeserialized['name'], - formatVersion: IndexTemplateFormatVersion -) { +export function useLoadIndexTemplate(name: TemplateDeserialized['name'], isLegacy?: boolean) { return useRequest({ - path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`, + path: `${API_BASE_PATH}/index-templates/${encodeURIComponent(name)}`, method: 'get', query: { - v: formatVersion, + legacy: isLegacy, }, }); } export async function saveTemplate(template: TemplateDeserialized, isClone?: boolean) { const result = await sendRequest({ - path: `${API_BASE_PATH}/templates`, - method: 'put', + path: `${API_BASE_PATH}/index-templates`, + method: 'post', body: JSON.stringify(template), }); @@ -266,7 +257,7 @@ export async function saveTemplate(template: TemplateDeserialized, isClone?: boo export async function updateTemplate(template: TemplateDeserialized) { const { name } = template; const result = await sendRequest({ - path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`, + path: `${API_BASE_PATH}/index-templates/${encodeURIComponent(name)}`, method: 'put', body: JSON.stringify(template), }); 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 fe118b1181082..a999c58f5bb42 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -3,33 +3,29 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { IndexTemplateFormatVersion } from '../../../common'; -export const getTemplateListLink = () => { - return `/templates`; -}; +export const getTemplateListLink = () => `/templates`; // Need to add some additonal encoding/decoding logic to work with React Router // For background, see: https://github.com/ReactTraining/history/issues/505 -export const getTemplateDetailsLink = ( - name: string, - formatVersion: IndexTemplateFormatVersion, - withHash = false -) => { - const baseUrl = `/templates/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}`; - const url = withHash ? `#${baseUrl}` : baseUrl; +export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHash = false) => { + const baseUrl = `/templates/${encodeURIComponent(encodeURIComponent(name))}`; + let url = withHash ? `#${baseUrl}` : baseUrl; + if (isLegacy) { + url = `${url}?legacy=${isLegacy}`; + } return encodeURI(url); }; -export const getTemplateEditLink = (name: string, formatVersion: IndexTemplateFormatVersion) => { +export const getTemplateEditLink = (name: string, isLegacy?: boolean) => { return encodeURI( - `/edit_template/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}` + `/edit_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` ); }; -export const getTemplateCloneLink = (name: string, formatVersion: IndexTemplateFormatVersion) => { +export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => { return encodeURI( - `/clone_template/${encodeURIComponent(encodeURIComponent(name))}?v=${formatVersion}` + `/clone_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` ); }; 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 1409fa8af2ceb..26e74847e3e05 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,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { TemplateDeserialized } from '../../../../common'; -import { serializeV1Template } from '../../../../common/lib'; +import { serializeLegacyTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { templateSchema } from './validate_schemas'; @@ -15,35 +15,26 @@ import { templateSchema } from './validate_schemas'; const bodySchema = templateSchema; export function registerCreateRoute({ router, license, lib }: RouteDependencies) { - router.put( - { path: addBasePath('/templates'), validate: { body: bodySchema } }, + router.post( + { path: addBasePath('/index-templates'), validate: { body: bodySchema } }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; const template = req.body as TemplateDeserialized; const { - _kbnMeta: { formatVersion }, + _kbnMeta: { isLegacy }, } = template; - if (formatVersion !== 1) { - return res.badRequest({ body: 'Only index template version 1 can be created.' }); + if (!isLegacy) { + return res.badRequest({ body: 'Only legacy index templates 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, - order, - index_patterns, - version, - settings, - mappings, - aliases, - } = serializedTemplate; + const serializedTemplate = serializeLegacyTemplate(template); + const { order, index_patterns, version, settings, mappings, aliases } = serializedTemplate; // Check that template with the same name doesn't already exist - const templateExists = await callAsCurrentUser('indices.existsTemplate', { name }); + const templateExists = await callAsCurrentUser('indices.existsTemplate', { + name: template.name, + }); if (templateExists) { return res.conflict({ @@ -51,7 +42,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) i18n.translate('xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage', { defaultMessage: "There is already a template with name '{name}'.", values: { - name, + name: template.name, }, }) ), @@ -61,7 +52,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) try { // Otherwise create new index template const response = await callAsCurrentUser('indices.putTemplate', { - name, + name: template.name, order, body: { index_patterns, 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 3dc31482b4947..b5cc00ad6d8cc 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 @@ -16,7 +16,7 @@ const bodySchema = schema.object({ templates: schema.arrayOf( schema.object({ name: schema.string(), - formatVersion: schema.oneOf([schema.literal(1), schema.literal(2)]), + isLegacy: schema.maybe(schema.boolean()), }) ), }); @@ -24,7 +24,7 @@ const bodySchema = schema.object({ export function registerDeleteRoute({ router, license }: RouteDependencies) { router.post( { - path: addBasePath('/delete-templates'), + path: addBasePath('/delete-index-templates'), validate: { body: bodySchema }, }, license.guardApiRoute(async (ctx, req, res) => { @@ -35,10 +35,10 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { }; await Promise.all( - templates.map(async ({ name, formatVersion }) => { + templates.map(async ({ name, isLegacy }) => { try { - if (formatVersion !== 1) { - return res.badRequest({ body: 'Only index template version 1 can be deleted.' }); + if (!isLegacy) { + return res.badRequest({ body: 'Only legacy index template can be deleted.' }); } await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.deleteTemplate', { 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 b18a8d88d3a4a..12ec005258a62 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 @@ -5,20 +5,38 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { deserializeV1Template, deserializeTemplateList } from '../../../../common/lib'; +import { + deserializeLegacyTemplate, + deserializeLegacyTemplateList, + deserializeTemplateList, +} from '../../../../common/lib'; import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; export function registerGetAllRoute({ router, license }: RouteDependencies) { router.get( - { path: addBasePath('/templates'), validate: false }, + { path: addBasePath('/index-templates'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser); - const indexTemplatesByName = await callAsCurrentUser('indices.getTemplate'); - const body = deserializeTemplateList(indexTemplatesByName, managedTemplatePrefix); + const _legacyTemplates = await callAsCurrentUser('indices.getTemplate'); + const { index_templates: _templates } = await callAsCurrentUser('transport.request', { + path: '_index_template', + method: 'GET', + }); + + const legacyTemplates = deserializeLegacyTemplateList( + _legacyTemplates, + managedTemplatePrefix + ); + const templates = deserializeTemplateList(_templates, managedTemplatePrefix); + + const body = { + templates, + legacyTemplates, + }; return res.ok({ body }); }) @@ -31,22 +49,22 @@ const paramsSchema = schema.object({ // 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')]), + legacy: schema.maybe(schema.boolean()), }); export function registerGetOneRoute({ router, license, lib }: RouteDependencies) { router.get( { - path: addBasePath('/templates/{name}'), + path: addBasePath('/index-templates/{name}'), validate: { params: paramsSchema, query: querySchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { name } = req.params as typeof paramsSchema.type; + const { name } = req.params as TypeOf; const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - const { v: version } = req.query as TypeOf; + const { legacy } = req.query as TypeOf; - if (version !== '1') { + if (!legacy) { return res.badRequest({ body: 'Only index template version 1 can be fetched.' }); } @@ -56,7 +74,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) if (indexTemplateByName[name]) { return res.ok({ - body: deserializeV1Template( + body: deserializeLegacyTemplate( { ...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 81d7aa1b4978c..5b2a0d8722e46 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 @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { TemplateDeserialized } from '../../../../common'; -import { serializeV1Template } from '../../../../common/lib'; +import { serializeLegacyTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { templateSchema } from './validate_schemas'; @@ -19,7 +19,7 @@ const paramsSchema = schema.object({ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) { router.put( { - path: addBasePath('/templates/{name}'), + path: addBasePath('/index-templates/{name}'), validate: { body: bodySchema, params: paramsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { @@ -27,14 +27,14 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) const { name } = req.params as typeof paramsSchema.type; const template = req.body as TemplateDeserialized; const { - _kbnMeta: { formatVersion }, + _kbnMeta: { isLegacy }, } = template; - if (formatVersion !== 1) { - return res.badRequest({ body: 'Only index template version 1 can be edited.' }); + if (!isLegacy) { + return res.badRequest({ body: 'Only legacy index template can be edited.' }); } - const serializedTemplate = serializeV1Template(template); + const serializedTemplate = serializeLegacyTemplate(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 491a686f81177..6ab28e9021123 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 @@ -24,8 +24,8 @@ export const templateSchema = schema.object({ rollover_alias: schema.maybe(schema.string()), }) ), - isManaged: schema.maybe(schema.boolean()), _kbnMeta: schema.object({ - formatVersion: schema.oneOf([schema.literal(1), schema.literal(2)]), + isManaged: schema.maybe(schema.boolean()), + isLegacy: schema.maybe(schema.boolean()), }), }); diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 055c32d5cd5e4..e2e93bfb365d4 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -5,7 +5,7 @@ */ import { getRandomString, getRandomNumber } from '../../../../test_utils'; -import { TemplateDeserialized, DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT } from '../../common'; +import { TemplateDeserialized } from '../../common'; export const getTemplate = ({ name = getRandomString(), @@ -14,10 +14,11 @@ export const getTemplate = ({ indexPatterns = [], template: { settings, aliases, mappings } = {}, isManaged = false, - templateFormatVersion = DEFAULT_INDEX_TEMPLATE_VERSION_FORMAT, + isLegacy = false, }: Partial< TemplateDeserialized & { - templateFormatVersion?: 1 | 2; + isLegacy?: boolean; + isManaged: boolean; } > = {}): TemplateDeserialized => ({ name, @@ -29,8 +30,8 @@ export const getTemplate = ({ mappings, settings, }, - isManaged, _kbnMeta: { - formatVersion: templateFormatVersion, + isManaged, + isLegacy, }, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5b017c6565c04..9da108abc6902 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6579,7 +6579,6 @@ "xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle": "まだテンプレートがありません", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription": "テンプレートを読み込み中…", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage": "テンプレートの読み込み中にエラーが発生", - "xpack.idxMgmt.indexTemplatesTable.systemIndexTemplatesSwitchLabel": "システムテンプレートを含める", "xpack.idxMgmt.licenseCheckErrorMessage": "ライセンス確認失敗", "xpack.idxMgmt.mappingsEditor.addFieldButtonLabel": "フィールドの追加", "xpack.idxMgmt.mappingsEditor.addMultiFieldTooltipLabel": "同じフィールドを異なる方法でインデックスするために、マルチフィールドを追加します。", @@ -7077,27 +7076,13 @@ "xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription": "クローンを作成するテンプレートを読み込み中…", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage": "クローンを作成するテンプレートを読み込み中にエラーが発生", "xpack.idxMgmt.templateDetails.aliasesTab.noAliasesTitle": "エイリアスが定義されていません。", - "xpack.idxMgmt.templateDetails.aliasesTabTitle": "エイリアス", - "xpack.idxMgmt.templateDetails.cloneButtonLabel": "クローンを作成", - "xpack.idxMgmt.templateDetails.closeButtonLabel": "閉じる", - "xpack.idxMgmt.templateDetails.deleteButtonLabel": "削除", - "xpack.idxMgmt.templateDetails.editButtonLabel": "編集", - "xpack.idxMgmt.templateDetails.loadingIndexTemplateDescription": "テンプレートを読み込み中…", - "xpack.idxMgmt.templateDetails.loadingIndexTemplateErrorMessage": "テンプレートの読み込み中にエラーが発生", - "xpack.idxMgmt.templateDetails.manageButtonLabel": "管理", - "xpack.idxMgmt.templateDetails.manageContextMenuPanelTitle": "テンプレートオプション", - "xpack.idxMgmt.templateDetails.managedTemplateInfoDescription": "マネージドテンプレートは内部オペレーションに不可欠です。", - "xpack.idxMgmt.templateDetails.managedTemplateInfoTitle": "マネジドテンプレートの編集は許可されていません。", "xpack.idxMgmt.templateDetails.mappingsTab.noMappingsTitle": "マッピングが定義されていません。", - "xpack.idxMgmt.templateDetails.mappingsTabTitle": "マッピング", "xpack.idxMgmt.templateDetails.settingsTab.noSettingsTitle": "設定が定義されていません。", - "xpack.idxMgmt.templateDetails.settingsTabTitle": "設定", "xpack.idxMgmt.templateDetails.summaryTab.ilmPolicyDescriptionListTitle": "ILM ポリシー", "xpack.idxMgmt.templateDetails.summaryTab.indexPatternsDescriptionListTitle": "インデックス{numIndexPatterns, plural, one {パターン} other {パターン}}", "xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText": "なし", "xpack.idxMgmt.templateDetails.summaryTab.orderDescriptionListTitle": "順序", "xpack.idxMgmt.templateDetails.summaryTab.versionDescriptionListTitle": "バージョン", - "xpack.idxMgmt.templateDetails.summaryTabTitle": "まとめ", "xpack.idxMgmt.templateEdit.loadingIndexTemplateDescription": "テンプレートを読み込み中…", "xpack.idxMgmt.templateEdit.loadingIndexTemplateErrorMessage": "テンプレートの読み込み中にエラーが発生", "xpack.idxMgmt.templateEdit.managedTemplateWarningDescription": "管理されているテンプレートは内部オペレーションに不可欠です。", @@ -7161,26 +7146,12 @@ "xpack.idxMgmt.templateForm.stepSettings.settingsDescription": "インデックスの動作を定義します。", "xpack.idxMgmt.templateForm.stepSettings.settingsEditorHelpText": "JSON フォーマットを使用: {code}", "xpack.idxMgmt.templateForm.stepSettings.stepTitle": "インデックス設定 (任意)", - "xpack.idxMgmt.templateList.table.actionCloneDescription": "このテンプレートのクローンを作成します", - "xpack.idxMgmt.templateList.table.actionCloneTitle": "クローンを作成", - "xpack.idxMgmt.templateList.table.actionColumnTitle": "アクション", - "xpack.idxMgmt.templateList.table.actionDeleteDecription": "このテンプレートを削除します", - "xpack.idxMgmt.templateList.table.actionDeleteText": "削除", - "xpack.idxMgmt.templateList.table.actionEditDecription": "このテンプレートを編集します", - "xpack.idxMgmt.templateList.table.actionEditText": "編集", - "xpack.idxMgmt.templateList.table.aliasesColumnTitle": "エイリアス", - "xpack.idxMgmt.templateList.table.createTemplatesButtonLabel": "テンプレートを作成", - "xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip": "管理されているテンプレートは削除できません。", - "xpack.idxMgmt.templateList.table.deleteTemplatesButtonLabel": "{count, plural, one {テンプレート} other {テンプレート} }を削除", "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "インデックスライフサイクルポリシー「{policyName}」", "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM ポリシー", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "インデックスパターン", - "xpack.idxMgmt.templateList.table.mappingsColumnTitle": "マッピング", "xpack.idxMgmt.templateList.table.nameColumnTitle": "名前", "xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "インデックステンプレートが見つかりません", - "xpack.idxMgmt.templateList.table.orderColumnTitle": "順序", "xpack.idxMgmt.templateList.table.reloadTemplatesButtonLabel": "再読み込み", - "xpack.idxMgmt.templateList.table.settingsColumnTitle": "設定", "xpack.idxMgmt.templateValidation.indexPatternsRequiredError": "インデックスパターンが最低 1 つ必要です。", "xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError": "テンプレート名に「{invalidChar}」は使用できません", "xpack.idxMgmt.templateValidation.templateNameLowerCaseRequiredError": "テンプレート名は小文字でなければなりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cee6860a58b5a..824f681786593 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6582,7 +6582,6 @@ "xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle": "您尚未有任何模板", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription": "正在加载模板……", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage": "加载模板时出错", - "xpack.idxMgmt.indexTemplatesTable.systemIndexTemplatesSwitchLabel": "包括系统模板", "xpack.idxMgmt.licenseCheckErrorMessage": "许可证检查失败", "xpack.idxMgmt.mappingsEditor.addFieldButtonLabel": "添加字段", "xpack.idxMgmt.mappingsEditor.addMultiFieldTooltipLabel": "添加多字段以使用不同的方式索引相同的字段。", @@ -7081,27 +7080,13 @@ "xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription": "正在加载要克隆的模板……", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage": "加载要克隆的模板时出错", "xpack.idxMgmt.templateDetails.aliasesTab.noAliasesTitle": "未定义任何别名。", - "xpack.idxMgmt.templateDetails.aliasesTabTitle": "别名", - "xpack.idxMgmt.templateDetails.cloneButtonLabel": "克隆", - "xpack.idxMgmt.templateDetails.closeButtonLabel": "关闭", - "xpack.idxMgmt.templateDetails.deleteButtonLabel": "删除", - "xpack.idxMgmt.templateDetails.editButtonLabel": "编辑", - "xpack.idxMgmt.templateDetails.loadingIndexTemplateDescription": "正在加载模板……", - "xpack.idxMgmt.templateDetails.loadingIndexTemplateErrorMessage": "加载模板时出错", - "xpack.idxMgmt.templateDetails.manageButtonLabel": "管理", - "xpack.idxMgmt.templateDetails.manageContextMenuPanelTitle": "模板选项", - "xpack.idxMgmt.templateDetails.managedTemplateInfoDescription": "托管模板对内部操作至关重要。", - "xpack.idxMgmt.templateDetails.managedTemplateInfoTitle": "不允许编辑托管模板", "xpack.idxMgmt.templateDetails.mappingsTab.noMappingsTitle": "未定义任何映射。", - "xpack.idxMgmt.templateDetails.mappingsTabTitle": "映射", "xpack.idxMgmt.templateDetails.settingsTab.noSettingsTitle": "未定义任何设置。", - "xpack.idxMgmt.templateDetails.settingsTabTitle": "设置", "xpack.idxMgmt.templateDetails.summaryTab.ilmPolicyDescriptionListTitle": "ILM 策略", "xpack.idxMgmt.templateDetails.summaryTab.indexPatternsDescriptionListTitle": "索引{numIndexPatterns, plural, one {模式} other {模式}}", "xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText": "无", "xpack.idxMgmt.templateDetails.summaryTab.orderDescriptionListTitle": "顺序", "xpack.idxMgmt.templateDetails.summaryTab.versionDescriptionListTitle": "版本", - "xpack.idxMgmt.templateDetails.summaryTabTitle": "总结", "xpack.idxMgmt.templateEdit.loadingIndexTemplateDescription": "正在加载模板……", "xpack.idxMgmt.templateEdit.loadingIndexTemplateErrorMessage": "加载模板时出错", "xpack.idxMgmt.templateEdit.managedTemplateWarningDescription": "托管模板对内部操作至关重要。", @@ -7165,26 +7150,12 @@ "xpack.idxMgmt.templateForm.stepSettings.settingsDescription": "定义索引的行为。", "xpack.idxMgmt.templateForm.stepSettings.settingsEditorHelpText": "使用 JSON 格式:{code}", "xpack.idxMgmt.templateForm.stepSettings.stepTitle": "索引设置(可选)", - "xpack.idxMgmt.templateList.table.actionCloneDescription": "克隆此模板", - "xpack.idxMgmt.templateList.table.actionCloneTitle": "克隆", - "xpack.idxMgmt.templateList.table.actionColumnTitle": "操作", - "xpack.idxMgmt.templateList.table.actionDeleteDecription": "删除此模板", - "xpack.idxMgmt.templateList.table.actionDeleteText": "删除", - "xpack.idxMgmt.templateList.table.actionEditDecription": "编辑此模板", - "xpack.idxMgmt.templateList.table.actionEditText": "编辑", - "xpack.idxMgmt.templateList.table.aliasesColumnTitle": "别名", - "xpack.idxMgmt.templateList.table.createTemplatesButtonLabel": "创建模板", - "xpack.idxMgmt.templateList.table.deleteManagedTemplateTooltip": "您无法删除托管模板。", - "xpack.idxMgmt.templateList.table.deleteTemplatesButtonLabel": "删除{count, plural, one {模板} other {模板} }", "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "“{policyName}”索引生命周期策略", "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM 策略", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "索引模式", - "xpack.idxMgmt.templateList.table.mappingsColumnTitle": "映射", "xpack.idxMgmt.templateList.table.nameColumnTitle": "名称", "xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "未找到任何索引模板", - "xpack.idxMgmt.templateList.table.orderColumnTitle": "顺序", "xpack.idxMgmt.templateList.table.reloadTemplatesButtonLabel": "重新加载", - "xpack.idxMgmt.templateList.table.settingsColumnTitle": "设置", "xpack.idxMgmt.templateValidation.indexPatternsRequiredError": "至少需要一个索引模式。", "xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError": "模板名称不得包含字符“{invalidChar}”", "xpack.idxMgmt.templateValidation.templateNameLowerCaseRequiredError": "模板名称必须小写。", 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 efe41ad47b019..9f1fab4039397 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 @@ -7,12 +7,12 @@ import { API_BASE_PATH, INDEX_PATTERNS } from './constants'; export const registerHelpers = ({ supertest }) => { - const getAllTemplates = () => supertest.get(`${API_BASE_PATH}/templates`); + const getAllTemplates = () => supertest.get(`${API_BASE_PATH}/index-templates`); - const getOneTemplate = (name, formatVersion = 1) => - supertest.get(`${API_BASE_PATH}/templates/${name}?v=${formatVersion}`); + const getOneTemplate = (name, isLegacy = true) => + supertest.get(`${API_BASE_PATH}/index-templates/${name}?legacy=${isLegacy}`); - const getTemplatePayload = (name, formatVersion = 1) => ({ + const getTemplatePayload = (name, isLegacy = true) => ({ name, order: 1, indexPatterns: INDEX_PATTERNS, @@ -45,19 +45,22 @@ export const registerHelpers = ({ supertest }) => { }, }, _kbnMeta: { - formatVersion, + isLegacy, }, }); const createTemplate = (payload) => - supertest.put(`${API_BASE_PATH}/templates`).set('kbn-xsrf', 'xxx').send(payload); + supertest.post(`${API_BASE_PATH}/index-templates`).set('kbn-xsrf', 'xxx').send(payload); const deleteTemplates = (templates) => - supertest.post(`${API_BASE_PATH}/delete-templates`).set('kbn-xsrf', 'xxx').send({ templates }); + supertest + .post(`${API_BASE_PATH}/delete-index-templates`) + .set('kbn-xsrf', 'xxx') + .send({ templates }); const updateTemplate = (payload, templateName) => supertest - .put(`${API_BASE_PATH}/templates/${templateName}`) + .put(`${API_BASE_PATH}/index-templates/${templateName}`) .set('kbn-xsrf', 'xxx') .send(payload); 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 625719443c56b..cd7f5fb209eca 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,10 +35,18 @@ export default function ({ getService }) { await createTemplate(payload).expect(200); }); + // TODO: When the "Create" API handler is added for V2 template, + // update this test to list composable templates. it('should list all the index templates with the expected parameters', async () => { - const { body: templates } = await getAllTemplates().expect(200); + const { body: allTemplates } = await getAllTemplates().expect(200); - const createdTemplate = templates.find((template) => template.name === payload.name); + // Composable templates + expect(allTemplates.templates).to.eql([]); + + // Legacy templates + const legacyTemplate = allTemplates.legacyTemplates.find( + (template) => template.name === payload.name + ); const expectedKeys = [ 'name', 'indexPatterns', @@ -46,13 +54,12 @@ export default function ({ getService }) { 'hasAliases', 'hasMappings', 'ilmPolicy', - 'isManaged', 'order', 'version', '_kbnMeta', ].sort(); - expect(Object.keys(createdTemplate).sort()).to.eql(expectedKeys); + expect(Object.keys(legacyTemplate).sort()).to.eql(expectedKeys); }); }); @@ -71,7 +78,6 @@ export default function ({ getService }) { 'indexPatterns', 'template', 'ilmPolicy', - 'isManaged', 'order', 'version', '_kbnMeta', @@ -155,7 +161,7 @@ export default function ({ getService }) { ).to.equal(templateName); const { body } = await deleteTemplates([ - { name: templateName, formatVersion: payload._kbnMeta.formatVersion }, + { name: templateName, isLegacy: payload._kbnMeta.isLegacy }, ]).expect(200); expect(body.errors).to.be.empty; diff --git a/x-pack/test_utils/stub_web_worker.ts b/x-pack/test_utils/stub_web_worker.ts index 2e7d5cf2098c8..10d0ee6517384 100644 --- a/x-pack/test_utils/stub_web_worker.ts +++ b/x-pack/test_utils/stub_web_worker.ts @@ -4,13 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -if (!window.Worker) { - // @ts-ignore we aren't honoring the real Worker spec here - window.Worker = function Worker() { - this.postMessage = jest.fn(); +function stubWebWorker() { + if (!window.Worker) { + // @ts-ignore we aren't honoring the real Worker spec here + window.Worker = function Worker() { + this.postMessage = jest.fn(); - // @ts-ignore TypeScript doesn't think this exists on the Worker interface - // https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate - this.terminate = jest.fn(); - }; + // @ts-ignore TypeScript doesn't think this exists on the Worker interface + // https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate + this.terminate = jest.fn(); + }; + } } + +stubWebWorker(); + +// Add an export to avoid TS complaining "stub_web_worker.ts" is not a module. +export { stubWebWorker };