diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts index efeadc75253c5..d2857d5f5f54b 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts @@ -10,6 +10,7 @@ import { TestBed, TestBedConfig, findTestSubject, + nextTick, } from '../../../../../../test_utils'; import { IndexManagementHome } from '../../../public/sections/home'; import { BASE_PATH } from '../../../common/constants'; @@ -28,9 +29,12 @@ const initTestBed = registerTestBed(IndexManagementHome, testBedConfig); export interface IdxMgmtHomeTestBed extends TestBed { actions: { - selectTab: (tab: 'indices' | 'index templates') => void; + selectHomeTab: (tab: 'indices' | 'index templates') => void; + selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void; clickReloadButton: () => void; clickTemplateActionAt: (index: number, action: 'delete') => void; + clickTemplateAt: (index: number) => void; + clickCloseDetailsButton: () => void; }; } @@ -41,7 +45,7 @@ export const setup = async (): Promise => { * User Actions */ - const selectTab = (tab: 'indices' | 'index templates') => { + const selectHomeTab = (tab: 'indices' | 'index templates') => { const tabs = ['indices', 'index templates']; testBed @@ -50,6 +54,15 @@ export const setup = async (): Promise => { .simulate('click'); }; + const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => { + const tabs = ['summary', 'settings', 'mappings', 'aliases']; + + testBed + .find('templateDetails.tab') + .at(tabs.indexOf(tab)) + .simulate('click'); + }; + const clickReloadButton = () => { const { find } = testBed; find('reloadButton').simulate('click'); @@ -69,12 +82,35 @@ export const setup = async (): Promise => { }); }; + const clickTemplateAt = async (index: number) => { + const { component, table, router } = testBed; + const { rows } = table.getMetaData('templatesTable'); + const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + const { href } = templateLink.props(); + router.navigateTo(href!); + await nextTick(); + component.update(); + }); + }; + + const clickCloseDetailsButton = () => { + const { find } = testBed; + + find('closeDetailsButton').simulate('click'); + }; + return { ...testBed, actions: { - selectTab, + selectHomeTab, + selectDetailsTab, clickReloadButton, clickTemplateActionAt, + clickTemplateAt, + clickCloseDetailsButton, }, }; }; @@ -82,19 +118,31 @@ export const setup = async (): Promise => { type IdxMgmtTestSubjects = TestSubjects; export type TestSubjects = + | 'aliasesTab' | 'appTitle' | 'cell' + | 'closeDetailsButton' | 'deleteSystemTemplateCallOut' | 'deleteTemplateButton' | 'deleteTemplatesButton' | 'deleteTemplatesConfirmation' | 'documentationLink' | 'emptyPrompt' + | 'mappingsTab' | 'indicesList' | 'reloadButton' | 'row' + | 'sectionError' | 'sectionLoading' + | 'settingsTab' + | 'summaryTab' + | 'summaryTitle' | 'systemTemplatesSwitch' | 'tab' + | 'templateDetails' + | 'templateDetails.deleteTemplateButton' + | 'templateDetails.sectionLoading' + | 'templateDetails.tab' + | 'templateDetails.title' | 'templatesList' | 'templatesTable'; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index ddcdc31b31598..90320740d09ae 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -36,10 +36,22 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadTemplateResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_PATH}/templates/:id`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadTemplatesResponse, setLoadIndicesResponse, setDeleteTemplateResponse, + setLoadTemplateResponse, }; }; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts index a66168c51e448..fbd0f6419979b 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts @@ -80,7 +80,7 @@ describe.skip('', () => { httpRequestsMockHelpers.setLoadTemplatesResponse([]); - actions.selectTab('index templates'); + actions.selectHomeTab('index templates'); // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { @@ -100,7 +100,7 @@ describe.skip('', () => { httpRequestsMockHelpers.setLoadTemplatesResponse([]); - actions.selectTab('index templates'); + actions.selectHomeTab('index templates'); // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { @@ -146,7 +146,7 @@ describe.skip('', () => { httpRequestsMockHelpers.setLoadTemplatesResponse(templates); - actions.selectTab('index templates'); + actions.selectHomeTab('index templates'); // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { @@ -220,6 +220,16 @@ describe.skip('', () => { expect(updatedRows.length).toEqual(templates.length); }); + test('each row should have a link to the template', async () => { + const { find, exists, actions } = testBed; + + await actions.clickTemplateAt(0); + + expect(exists('templatesList')).toBe(true); + expect(exists('templateDetails')).toBe(true); + expect(find('templateDetails.title').text()).toBe(template1.name); + }); + describe('delete index template', () => { test('should have action buttons on each row to delete an index template', () => { const { table } = testBed; @@ -298,6 +308,221 @@ describe.skip('', () => { expect(latestRequest.url).toBe(`${API_PATH}/templates/${template1.name}`); }); }); + + describe('detail flyout', () => { + it('should have a close button and be able to close flyout', async () => { + const template = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + }); + + const { actions, component, exists } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(template); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('closeDetailsButton')).toBe(true); + expect(exists('summaryTab')).toBe(true); + + actions.clickCloseDetailsButton(); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('summaryTab')).toBe(false); + }); + + it('should have a delete button', async () => { + const template = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + }); + + const { actions, component, exists } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(template); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('templateDetails.deleteTemplateButton')).toBe(true); + }); + + it('should render an error if error fetching template details', async () => { + const { actions, component, exists } = testBed; + const error = { + status: 404, + error: 'Not found', + message: 'Template not found', + }; + + httpRequestsMockHelpers.setLoadTemplateResponse(undefined, { body: error }); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('sectionError')).toBe(true); + // Delete button should not render if error + expect(exists('templateDetails.deleteTemplateButton')).toBe(false); + }); + + describe('tabs', () => { + test('should have 4 tabs if template has mappings, settings and aliases data', async () => { + const template = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + settings: { + index: { + number_of_shards: '1', + }, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + aliases: { + alias1: {}, + }, + }); + + const { find, actions, exists, component } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(template); + + await actions.clickTemplateAt(0); + + expect(find('templateDetails.tab').length).toBe(4); + expect(find('templateDetails.tab').map(t => t.text())).toEqual([ + 'Summary', + 'Settings', + 'Mappings', + 'Aliases', + ]); + + // Summary tab should be initial active tab + expect(exists('summaryTab')).toBe(true); + + // Navigate and verify all tabs + actions.selectDetailsTab('settings'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTab')).toBe(true); + + actions.selectDetailsTab('aliases'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTab')).toBe(false); + expect(exists('aliasesTab')).toBe(true); + + actions.selectDetailsTab('mappings'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTab')).toBe(false); + expect(exists('aliasesTab')).toBe(false); + expect(exists('mappingsTab')).toBe(true); + }); + + it('should not show tabs if mappings, settings and aliases data is not present', async () => { + const templateWithNoTabs = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + }); + + const { actions, find, exists, component } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoTabs); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(find('templateDetails.tab').length).toBe(0); + expect(exists('summaryTab')).toBe(true); + expect(exists('summaryTitle')).toBe(true); + }); + + it('should not show all tabs if mappings, settings or aliases data is not present', async () => { + const templateWithSomeTabs = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + settings: { + index: { + number_of_shards: '1', + }, + }, + }); + + const { actions, find, exists, component } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(templateWithSomeTabs); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(find('templateDetails.tab').length).toBe(2); + expect(exists('summaryTab')).toBe(true); + // Template does not contain aliases or mappings, so tabs will not render + expect(find('templateDetails.tab').map(t => t.text())).toEqual([ + 'Summary', + 'Settings', + ]); + }); + }); + }); }); }); }); diff --git a/x-pack/legacy/plugins/index_management/common/constants/index.ts b/x-pack/legacy/plugins/index_management/common/constants/index.ts index 9a122ecee63ef..06159fd45ede1 100644 --- a/x-pack/legacy/plugins/index_management/common/constants/index.ts +++ b/x-pack/legacy/plugins/index_management/common/constants/index.ts @@ -40,4 +40,9 @@ export { UIM_TEMPLATE_LIST_LOAD, UIM_TEMPLATE_DELETE, UIM_TEMPLATE_DELETE_MANY, + UIM_TEMPLATE_SHOW_DETAILS_CLICK, + UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, } from './ui_metric'; diff --git a/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts b/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts index 962b39bfab196..7f0c62ddf5ec0 100644 --- a/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts +++ b/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts @@ -36,3 +36,8 @@ export const UIM_DETAIL_PANEL_SUMMARY_TAB = 'detail_panel_summary_tab'; export const UIM_TEMPLATE_LIST_LOAD = 'template_list_load'; export const UIM_TEMPLATE_DELETE = 'template_delete'; export const UIM_TEMPLATE_DELETE_MANY = 'template_delete_many'; +export const UIM_TEMPLATE_SHOW_DETAILS_CLICK = 'template_show_details_click'; +export const UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB = 'template_details_summary_tab'; +export const UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB = 'template_details_settings_tab'; +export const UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB = 'template_details_mappings_tab'; +export const UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB = 'template_details_aliases_tab'; diff --git a/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx b/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx index cb3ac528ba682..926be62739265 100644 --- a/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx @@ -21,10 +21,6 @@ export const DeleteTemplatesModal = ({ }) => { const numTemplatesToDelete = templatesToDelete.length; - if (!numTemplatesToDelete) { - return null; - } - const hasSystemTemplate = Boolean( templatesToDelete.find(templateName => templateName.startsWith('.')) ); diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx index 2784f049ba028..5d2fb54216da7 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx +++ b/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx @@ -104,7 +104,7 @@ export const IndexManagementHome: React.FunctionComponent - + diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/index.ts b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/index.ts new file mode 100644 index 0000000000000..7360c96c250cf --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/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 { TemplateDetails } from './template_details'; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/aliases_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/aliases_tab.tsx new file mode 100644 index 0000000000000..02cb59619aae1 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/aliases_tab.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCodeEditor } from '@elastic/eui'; +import { Template } from '../../../../../../common/types'; + +interface Props { + templateDetails: Template; +} + +export const AliasesTab: React.FunctionComponent = ({ templateDetails }) => { + const { aliases } = templateDetails; + const aliasesJsonString = JSON.stringify(aliases, null, 2); + + return ( +
+ +
+ ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/index.ts b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/index.ts new file mode 100644 index 0000000000000..a648a7f476312 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SummaryTab } from './summary_tab'; +export { MappingsTab } from './mappings_tab'; +export { SettingsTab } from './settings_tab'; +export { AliasesTab } from './aliases_tab'; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/mappings_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/mappings_tab.tsx new file mode 100644 index 0000000000000..15133e595a280 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/mappings_tab.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCodeEditor } from '@elastic/eui'; +import { Template } from '../../../../../../common/types'; + +interface Props { + templateDetails: Template; +} + +export const MappingsTab: React.FunctionComponent = ({ templateDetails }) => { + const { mappings } = templateDetails; + const mappingsJsonString = JSON.stringify(mappings, null, 2); + + return ( +
+ +
+ ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/settings_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/settings_tab.tsx new file mode 100644 index 0000000000000..697c2e4ab5272 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/settings_tab.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCodeEditor } from '@elastic/eui'; +import { Template } from '../../../../../../common/types'; + +interface Props { + templateDetails: Template; +} + +export const SettingsTab: React.FunctionComponent = ({ templateDetails }) => { + const { settings } = templateDetails; + const settingsJsonString = JSON.stringify(settings, null, 2); + + return ( +
+ +
+ ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/summary_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/summary_tab.tsx new file mode 100644 index 0000000000000..3d55bdf6ab977 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/summary_tab.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { Template } from '../../../../../../common/types'; +import { getILMPolicyPath } from '../../../../../services/navigation'; + +interface Props { + templateDetails: Template; +} + +const NoneDescriptionText = () => ( + +); + +export const SummaryTab: React.FunctionComponent = ({ templateDetails }) => { + const { version, order, indexPatterns = [], settings } = templateDetails; + + const ilmPolicy = settings && settings.index && settings.index.lifecycle; + const numIndexPatterns = indexPatterns.length; + + return ( + + + + + + + + {numIndexPatterns > 1 ? ( + +
    + {indexPatterns.map((indexName: string, i: number) => { + return ( +
  • + + {indexName} + +
  • + ); + })} +
+
+ ) : ( + indexPatterns.toString() + )} +
+
+
+ + + + + + + + {ilmPolicy && ilmPolicy.name ? ( + {ilmPolicy.name} + ) : ( + + )} + + + + + + {typeof order !== 'undefined' ? order : } + + + + + + {typeof version !== 'undefined' ? version : } + + + +
+ ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/template_details.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/template_details.tsx new file mode 100644 index 0000000000000..9c295fbcbc2d3 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/template_details.tsx @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiTab, + EuiTabs, + EuiSpacer, +} from '@elastic/eui'; +import { + UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +} from '../../../../../common/constants'; +import { Template } from '../../../../../common/types'; +import { DeleteTemplatesModal, SectionLoading, SectionError } from '../../../../components'; +import { loadIndexTemplate } from '../../../../services/api'; +import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric'; +import { SummaryTab, MappingsTab, SettingsTab, AliasesTab } from './tabs'; + +interface Props { + templateName: Template['name']; + onClose: () => void; + reload: () => Promise; +} + +const SUMMARY_TAB_ID = 'summary'; +const MAPPINGS_TAB_ID = 'mappings'; +const ALIASES_TAB_ID = 'aliases'; +const SETTINGS_TAB_ID = 'settings'; + +const summaryTabData = { + id: SUMMARY_TAB_ID, + name: ( + + ), +}; + +const settingsTabData = { + id: SETTINGS_TAB_ID, + name: ( + + ), +}; + +const mappingsTabData = { + id: MAPPINGS_TAB_ID, + name: ( + + ), +}; + +const aliasesTabData = { + id: ALIASES_TAB_ID, + name: ( + + ), +}; + +const tabToComponentMap: { + [key: string]: React.FunctionComponent<{ templateDetails: Template }>; +} = { + [SUMMARY_TAB_ID]: SummaryTab, + [SETTINGS_TAB_ID]: SettingsTab, + [MAPPINGS_TAB_ID]: MappingsTab, + [ALIASES_TAB_ID]: AliasesTab, +}; + +const tabToUiMetricMap: { [key: string]: string } = { + [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +}; + +const hasEntries = (tabData: object) => { + return tabData ? Object.entries(tabData).length > 0 : false; +}; + +export const TemplateDetails: React.FunctionComponent = ({ + templateName, + onClose, + reload, +}) => { + const { error, data: templateDetails, isLoading } = loadIndexTemplate(templateName); + + const [templateToDelete, setTemplateToDelete] = useState>([]); + const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error} + data-test-subj="sectionError" + /> + ); + } else if (templateDetails) { + const { settings, mappings, aliases } = templateDetails; + + const settingsTab = hasEntries(settings) ? [settingsTabData] : []; + const mappingsTab = hasEntries(mappings) ? [mappingsTabData] : []; + const aliasesTab = hasEntries(aliases) ? [aliasesTabData] : []; + + const optionalTabs = [...settingsTab, ...mappingsTab, ...aliasesTab]; + const tabs = optionalTabs.length > 0 ? [summaryTabData, ...optionalTabs] : []; + + if (tabs.length > 0) { + const Content = tabToComponentMap[activeTab]; + + content = ( + + + {tabs.map(tab => ( + { + trackUiMetric(METRIC_TYPE.CLICK, tabToUiMetricMap[tab.id]); + setActiveTab(tab.id); + }} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subj="tab" + > + {tab.name} + + ))} + + + + + + + ); + } else { + content = ( + + +

+ +

+
+ + + + +
+ ); + } + } + + return ( + + {templateToDelete.length ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } + setTemplateToDelete([]); + onClose(); + }} + templatesToDelete={templateToDelete} + /> + ) : null} + + + + +

+ {templateName} +

+
+
+ + {content} + + + + + + + + + + {templateDetails && ( + + + + + + )} + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx index a17bfaf1ce27a..51fa92582d573 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx @@ -5,6 +5,7 @@ */ import React, { Fragment, useState, useEffect, useMemo } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt, @@ -20,9 +21,19 @@ import { TemplatesTable } from './templates_table'; import { loadIndexTemplates } from '../../../services/api'; import { Template } from '../../../../common/types'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; -import { UIM_TEMPLATE_LIST_LOAD } from '../../../../common/constants'; +import { UIM_TEMPLATE_LIST_LOAD, BASE_PATH } from '../../../../common/constants'; +import { TemplateDetails } from './template_details'; -export const TemplatesList: React.FunctionComponent = () => { +interface MatchParams { + templateName?: Template['name']; +} + +export const TemplatesList: React.FunctionComponent> = ({ + match: { + params: { templateName }, + }, + history, +}) => { const { error, isLoading, data: templates, createRequest: reload } = loadIndexTemplates(); let content; @@ -36,6 +47,10 @@ export const TemplatesList: React.FunctionComponent = () => { [templates] ); + const closeTemplateDetails = () => { + history.push(`${BASE_PATH}templates`); + }; + // Track component loaded useEffect(() => { trackUiMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD); @@ -115,5 +130,16 @@ export const TemplatesList: React.FunctionComponent = () => { ); } - return
{content}
; + return ( +
+ {content} + {templateName && ( + + )} +
+ ); }; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx index c237d26021bec..8025b89f9d9fd 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx @@ -7,9 +7,18 @@ import React, { useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiInMemoryTable, EuiIcon, EuiButton, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { + EuiInMemoryTable, + EuiIcon, + EuiButton, + EuiToolTip, + EuiButtonIcon, + EuiLink, +} from '@elastic/eui'; import { Template } from '../../../../../common/types'; +import { BASE_PATH, UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../common/constants'; import { DeleteTemplatesModal } from '../../../../components'; +import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric'; interface Props { templates: Template[]; @@ -34,6 +43,17 @@ export const TemplatesTable: React.FunctionComponent = ({ templates, relo }), truncateText: true, sortable: true, + render: (name: Template['name']) => { + return ( + trackUiMetric(METRIC_TYPE.CLICK, UIM_TEMPLATE_SHOW_DETAILS_CLICK)} + > + {name} + + ); + }, }, { field: 'indexPatterns', @@ -206,15 +226,17 @@ export const TemplatesTable: React.FunctionComponent = ({ templates, relo return ( - { - if (data && data.hasDeletedTemplates) { - reload(); - } - setTemplatesToDelete([]); - }} - templatesToDelete={templatesToDelete} - /> + {templatesToDelete.length ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } + setTemplatesToDelete([]); + }} + templatesToDelete={templatesToDelete} + /> + ) : null} ) => { uimActionType, }); }; + +export function loadIndexTemplate(name: Template['name']) { + return useRequest({ + path: `${apiPrefix}/templates/${encodeURIComponent(name)}`, + method: 'get', + }); +} diff --git a/x-pack/legacy/plugins/index_management/public/services/navigation.js b/x-pack/legacy/plugins/index_management/public/services/navigation.ts similarity index 69% rename from x-pack/legacy/plugins/index_management/public/services/navigation.js rename to x-pack/legacy/plugins/index_management/public/services/navigation.ts index 45e4c73d8643d..16072cb702f00 100644 --- a/x-pack/legacy/plugins/index_management/public/services/navigation.js +++ b/x-pack/legacy/plugins/index_management/public/services/navigation.ts @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import { BASE_PATH } from '../../common/constants'; -let urlService; -export const setUrlService = (aUrlService) => { + +let urlService: any; + +export const setUrlService = (aUrlService: any) => { urlService = aUrlService; }; + export const getUrlService = () => { return urlService; }; -export const getIndexListUri = (filter) => { - if(filter) { + +export const getIndexListUri = (filter: any) => { + if (filter) { // React router tries to decode url params but it can't because the browser partially // decodes them. So we have to encode both the URL and the filter to get it all to // work correctly for filters with URL unsafe characters in them. @@ -22,3 +26,11 @@ export const getIndexListUri = (filter) => { // If no filter, URI is already safe so no need to encode. return `#${BASE_PATH}indices`; }; + +export const getILMPolicyPath = (policyName: string) => { + return encodeURI( + `#/management/elasticsearch/index_lifecycle_management/policies/edit/${encodeURIComponent( + policyName + )}` + ); +}; diff --git a/x-pack/legacy/plugins/index_management/server/lib/fetch_templates.ts b/x-pack/legacy/plugins/index_management/server/lib/fetch_templates.ts deleted file mode 100644 index 673c4569f21c0..0000000000000 --- a/x-pack/legacy/plugins/index_management/server/lib/fetch_templates.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const fetchTemplates = async (callWithRequest: any) => { - const indexTemplatesByName = await callWithRequest('indices.getTemplate'); - const indexTemplateNames = Object.keys(indexTemplatesByName); - - const indexTemplates = indexTemplateNames.map(name => { - const { - version, - order, - index_patterns: indexPatterns = [], - settings = {}, - aliases = {}, - mappings = {}, - } = indexTemplatesByName[name]; - return { - name, - version, - order, - indexPatterns: indexPatterns.sort(), - settings, - aliases, - mappings, - }; - }); - - return indexTemplates; -}; diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts new file mode 100644 index 0000000000000..7ed94ba73b13c --- /dev/null +++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router'; + +const allHandler: RouterRouteHandler = async (_req, callWithRequest) => { + const indexTemplatesByName = await callWithRequest('indices.getTemplate'); + const indexTemplateNames = Object.keys(indexTemplatesByName); + + const indexTemplates = indexTemplateNames.map(name => { + const { + version, + order, + index_patterns: indexPatterns = [], + settings = {}, + aliases = {}, + mappings = {}, + } = indexTemplatesByName[name]; + return { + name, + version, + order, + indexPatterns: indexPatterns.sort(), + settings, + aliases, + mappings, + }; + }); + + return indexTemplates; +}; + +const oneHandler: RouterRouteHandler = async (req, callWithRequest) => { + const { name } = req.params; + const indexTemplateByName = await callWithRequest('indices.getTemplate', { name }); + + if (indexTemplateByName[name]) { + const { + version, + order, + index_patterns: indexPatterns = [], + settings = {}, + aliases = {}, + mappings = {}, + } = indexTemplateByName[name]; + + return { + name, + version, + order, + indexPatterns: indexPatterns.sort(), + settings, + aliases, + mappings, + }; + } +}; + +export function registerGetAllRoute(router: Router) { + router.get('templates', allHandler); +} + +export function registerGetOneRoute(router: Router) { + router.get('templates/{name}', oneHandler); +} diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_list_route.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_list_route.ts deleted file mode 100644 index c951c2d62330d..0000000000000 --- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_list_route.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router'; -import { fetchTemplates } from '../../../lib/fetch_templates'; - -const handler: RouterRouteHandler = async (_req, callWithRequest) => { - return fetchTemplates(callWithRequest); -}; - -export function registerListRoute(router: Router) { - router.get('templates', handler); -} diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts index 80b59ef5ce0f9..084abd0a91eb3 100644 --- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts +++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts @@ -5,12 +5,13 @@ */ import { Router } from '../../../../../../server/lib/create_router'; -import { registerListRoute } from './register_list_route'; +import { registerGetAllRoute, registerGetOneRoute } from './register_get_routes'; import { registerDeleteRoute } from './register_delete_route'; import { registerCreateRoute } from './register_create_route'; export function registerTemplatesRoutes(router: Router) { - registerListRoute(router); + registerGetAllRoute(router); + registerGetOneRoute(router); registerDeleteRoute(router); registerCreateRoute(router); } 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 f1773441fd3a8..e5a696aa39ac9 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,7 +7,9 @@ import { API_BASE_PATH, INDEX_PATTERNS } from './constants'; export const registerHelpers = ({ supertest }) => { - const list = () => supertest.get(`${API_BASE_PATH}/templates`); + const getAllTemplates = () => supertest.get(`${API_BASE_PATH}/templates`); + + const getOneTemplate = name => supertest.get(`${API_BASE_PATH}/templates/${name}`); const getTemplatePayload = name => ({ name, @@ -48,7 +50,8 @@ export const registerHelpers = ({ supertest }) => { .set('kbn-xsrf', 'xxx'); return { - list, + getAllTemplates, + getOneTemplate, getTemplatePayload, createTemplate, deleteTemplates, diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index 0c6e4c5cb2d37..517966d342232 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 @@ -19,7 +19,8 @@ export default function ({ getService }) { } = initElasticsearchHelpers(es); const { - list, + getAllTemplates, + getOneTemplate, createTemplate, getTemplatePayload, deleteTemplates, @@ -28,15 +29,32 @@ export default function ({ getService }) { describe('index templates', () => { after(() => Promise.all([cleanUpEsResources()])); - describe('list', function () { - it('should list all the index templates with the expected properties', async function () { - const { body } = await list().expect(200); + describe('get all', () => { + it('should list all the index templates with the expected properties', async () => { + const { body } = await getAllTemplates().expect(200); const expectedKeys = ['name', 'indexPatterns', 'settings', 'aliases', 'mappings']; expectedKeys.forEach(key => expect(Object.keys(body[0]).includes(key)).to.be(true)); }); }); + describe('get one', () => { + const templateName = `template-${getRandomString()}`; + const payload = getTemplatePayload(templateName); + + beforeEach(async () => { + await createTemplate(payload).expect(200); + }); + + it('should list the index template with the expected properties', async () => { + const { body } = await getOneTemplate(templateName).expect(200); + const expectedKeys = ['name', 'indexPatterns', 'settings', 'aliases', 'mappings']; + + expect(body.name).to.equal(templateName); + expectedKeys.forEach(key => expect(Object.keys(body).includes(key)).to.be(true)); + }); + }); + describe('create', () => { it('should create an index template', async () => { const templateName = `template-${getRandomString()}`;