diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts new file mode 100644 index 0000000000000..7c17dde119c42 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts @@ -0,0 +1,230 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { setupEnvironment, pageHelpers } from './helpers'; +import { ComponentTemplateDetailsTestBed } from './helpers/component_template_details.helpers'; +import { ComponentTemplateDeserialized } from '../../shared_imports'; + +const { setup } = pageHelpers.componentTemplateDetails; + +jest.mock('ui/i18n', () => { + const I18nContext = ({ children }: any) => children; + return { I18nContext }; +}); + +const COMPONENT_TEMPLATE: ComponentTemplateDeserialized = { + name: 'comp-1', + template: { + mappings: { properties: { ip_address: { type: 'ip' } } }, + aliases: { mydata: {} }, + settings: { number_of_shards: 1 }, + }, + version: 1, + _meta: { description: 'component template test' }, + _kbnMeta: { usedBy: ['template_1'] }, +}; + +const COMPONENT_TEMPLATE_ONLY_REQUIRED_FIELDS: ComponentTemplateDeserialized = { + name: 'comp-base', + template: {}, + _kbnMeta: { usedBy: [] }, +}; + +describe('', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: ComponentTemplateDetailsTestBed; + + afterAll(() => { + server.restore(); + }); + + describe('With component template details', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse(COMPONENT_TEMPLATE); + + await act(async () => { + testBed = setup({ + componentTemplateName: COMPONENT_TEMPLATE.name, + onClose: () => {}, + }); + }); + + testBed.component.update(); + }); + + test('renders the details flyout', () => { + const { exists, find, actions, component } = testBed; + + // Verify flyout exists with correct title + expect(exists('componentTemplateDetails')).toBe(true); + expect(find('componentTemplateDetails.title').text()).toBe(COMPONENT_TEMPLATE.name); + + // Verify footer does not display since "actions" prop was not provided + expect(exists('componentTemplateDetails.footer')).toBe(false); + + // Verify tabs exist + expect(exists('settingsTab')).toBe(true); + expect(exists('mappingsTab')).toBe(true); + expect(exists('aliasesTab')).toBe(true); + // Summary tab should be active by default + expect(find('summaryTab').props()['aria-selected']).toBe(true); + + // [Summary tab] Verify description list items + expect(exists('summaryTabContent.usedByTitle')).toBe(true); + expect(exists('summaryTabContent.versionTitle')).toBe(true); + expect(exists('summaryTabContent.metaTitle')).toBe(true); + + // [Settings tab] Navigate to tab and verify content + act(() => { + actions.clickSettingsTab(); + }); + + component.update(); + + expect(exists('settingsTabContent')).toBe(true); + + // [Mappings tab] Navigate to tab and verify content + act(() => { + actions.clickMappingsTab(); + }); + + component.update(); + expect(exists('mappingsTabContent')).toBe(true); + + // [Aliases tab] Navigate to tab and verify content + act(() => { + actions.clickAliasesTab(); + }); + + component.update(); + expect(exists('aliasesTabContent')).toBe(true); + }); + }); + + describe('With only required component template fields', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse( + COMPONENT_TEMPLATE_ONLY_REQUIRED_FIELDS + ); + + await act(async () => { + testBed = setup({ + componentTemplateName: COMPONENT_TEMPLATE_ONLY_REQUIRED_FIELDS.name, + onClose: () => {}, + }); + }); + + testBed.component.update(); + }); + + test('renders the details flyout', () => { + const { exists, actions, component } = testBed; + + // [Summary tab] Verify optional description list items do not display + expect(exists('summaryTabContent.usedByTitle')).toBe(false); + expect(exists('summaryTabContent.versionTitle')).toBe(false); + expect(exists('summaryTabContent.metaTitle')).toBe(false); + // Verify callout renders indicating the component template is not in use + expect(exists('notInUseCallout')).toBe(true); + + // [Settings tab] Navigate to tab and verify info callout + act(() => { + actions.clickSettingsTab(); + }); + + component.update(); + + expect(exists('noSettingsCallout')).toBe(true); + + // [Mappings tab] Navigate to tab and verify info callout + act(() => { + actions.clickMappingsTab(); + }); + + component.update(); + expect(exists('noMappingsCallout')).toBe(true); + + // [Aliases tab] Navigate to tab and verify info callout + act(() => { + actions.clickAliasesTab(); + }); + + component.update(); + expect(exists('noAliasesCallout')).toBe(true); + }); + }); + + describe('With actions', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse(COMPONENT_TEMPLATE); + + await act(async () => { + testBed = setup({ + componentTemplateName: COMPONENT_TEMPLATE.name, + onClose: () => {}, + actions: [ + { + name: 'Test', + icon: 'info', + closePopoverOnClick: true, + handleActionClick: () => {}, + }, + ], + }); + }); + + testBed.component.update(); + }); + + test('should render a footer with context menu', () => { + const { exists, actions, component, find } = testBed; + + // Verify footer exists + expect(exists('componentTemplateDetails.footer')).toBe(true); + expect(exists('manageComponentTemplateButton')).toBe(true); + + // Click manage button and verify actions + act(() => { + actions.clickManageButton(); + }); + + component.update(); + + expect(exists('manageComponentTemplateContextMenu')).toBe(true); + expect(find('manageComponentTemplateContextMenu.action').length).toEqual(1); + }); + }); + + describe('Error handling', () => { + const error = { + status: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse(undefined, { body: error }); + + await act(async () => { + testBed = setup({ + componentTemplateName: COMPONENT_TEMPLATE.name, + onClose: () => {}, + }); + }); + + testBed.component.update(); + }); + + test('should render an error message if error fetching pipelines', async () => { + const { exists, find } = testBed; + + expect(exists('sectionError')).toBe(true); + expect(find('sectionError').text()).toContain('Error loading component template'); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts index 830cc0ee6a980..86eb88017b77f 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts @@ -6,10 +6,11 @@ import { act } from 'react-dom/test-utils'; +import { ComponentTemplateListItem } from '../../shared_imports'; + import { setupEnvironment, pageHelpers } from './helpers'; import { ComponentTemplateListTestBed } from './helpers/component_template_list.helpers'; -import { API_BASE_PATH } from '../../../../../../common/constants'; -import { ComponentTemplateListItem } from '../../types'; +import { API_BASE_PATH } from './helpers/constants'; const { setup } = pageHelpers.componentTemplateList; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts new file mode 100644 index 0000000000000..25c2d654fd900 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_details.helpers.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerTestBed, TestBed } from '../../../../../../../../../test_utils'; +import { WithAppDependencies } from './setup_environment'; +import { ComponentTemplateDetailsFlyout } from '../../../component_template_details'; + +export type ComponentTemplateDetailsTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + const { find } = testBed; + + /** + * User Actions + */ + const clickSettingsTab = () => { + find('settingsTab').simulate('click'); + }; + + const clickMappingsTab = () => { + find('mappingsTab').simulate('click'); + }; + + const clickAliasesTab = () => { + find('aliasesTab').simulate('click'); + }; + + const clickManageButton = () => { + find('manageComponentTemplateButton').simulate('click'); + }; + + return { + clickSettingsTab, + clickAliasesTab, + clickMappingsTab, + clickManageButton, + }; +}; + +export const setup = (props: any): ComponentTemplateDetailsTestBed => { + const setupTestBed = registerTestBed( + WithAppDependencies(ComponentTemplateDetailsFlyout), + { + memoryRouter: { + wrapComponent: false, + }, + defaultProps: props, + } + ); + + const testBed = setupTestBed() as ComponentTemplateDetailsTestBed; + + return { + ...testBed, + actions: createActions(testBed), + }; +}; + +export type ComponentTemplateDetailsTestSubjects = + | 'componentTemplateDetails' + | 'componentTemplateDetails.title' + | 'componentTemplateDetails.footer' + | 'summaryTab' + | 'mappingsTab' + | 'settingsTab' + | 'aliasesTab' + | 'sectionError' + | 'summaryTabContent' + | 'summaryTabContent.usedByTitle' + | 'summaryTabContent.versionTitle' + | 'summaryTabContent.metaTitle' + | 'notInUseCallout' + | 'aliasesTabContent' + | 'noAliasesCallout' + | 'mappingsTabContent' + | 'noMappingsCallout' + | 'settingsTabContent' + | 'noSettingsCallout' + | 'manageComponentTemplateButton' + | 'manageComponentTemplateContextMenu' + | 'manageComponentTemplateContextMenu.action'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/constants.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/constants.ts new file mode 100644 index 0000000000000..00b07fadd0c08 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/constants.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const API_BASE_PATH = '/api/index_management'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts index 8473041ee0af3..b7b674292dd98 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts @@ -5,11 +5,15 @@ */ import sinon, { SinonFakeServer } from 'sinon'; -import { API_BASE_PATH } from '../../../../../../../common'; +import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../../../shared_imports'; +import { API_BASE_PATH } from './constants'; // Register helpers to mock HTTP Requests const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setLoadComponentTemplatesResponse = (response?: any[], error?: any) => { + const setLoadComponentTemplatesResponse = ( + response?: ComponentTemplateListItem[], + error?: any + ) => { const status = error ? error.status || 400 : 200; const body = error ? error.body : response; @@ -20,6 +24,20 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadComponentTemplateResponse = ( + response?: ComponentTemplateDeserialized, + error?: any + ) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_BASE_PATH}/component_templates/:name`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setDeleteComponentTemplateResponse = (response?: object) => { server.respondWith('DELETE', `${API_BASE_PATH}/component_templates/:name`, [ 200, @@ -31,6 +49,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { return { setLoadComponentTemplatesResponse, setDeleteComponentTemplateResponse, + setLoadComponentTemplateResponse, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts index c1d75b3c2dd9b..93eb65aac0761 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts @@ -5,6 +5,7 @@ */ import { setup as componentTemplatesListSetup } from './component_template_list.helpers'; +import { setup as componentTemplateDetailsSetup } from './component_template_details.helpers'; export { nextTick, getRandomString, findTestSubject } from '../../../../../../../../../test_utils'; @@ -12,4 +13,5 @@ export { setupEnvironment } from './setup_environment'; export const pageHelpers = { componentTemplateList: { setup: componentTemplatesListSetup }, + componentTemplateDetails: { setup: componentTemplateDetailsSetup }, }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx index c0aeb70166b5b..a2194bbfa0186 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx @@ -9,21 +9,21 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { HttpSetup } from 'kibana/public'; -import { BASE_PATH, API_BASE_PATH } from '../../../../../../../common/constants'; import { notificationServiceMock, docLinksServiceMock, } from '../../../../../../../../../../src/core/public/mocks'; -import { init as initHttpRequests } from './http_requests'; import { ComponentTemplatesProvider } from '../../../component_templates_context'; +import { init as initHttpRequests } from './http_requests'; +import { API_BASE_PATH } from './constants'; + const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); const appDependencies = { httpClient: (mockHttpClient as unknown) as HttpSetup, apiBasePath: API_BASE_PATH, - appBasePath: BASE_PATH, trackMetric: () => {}, docLinks: docLinksServiceMock.createStartContract(), toasts: notificationServiceMock.createSetupContract().toasts, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx new file mode 100644 index 0000000000000..a8007c6363584 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx @@ -0,0 +1,150 @@ +/* + * 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 { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiCallOut, +} from '@elastic/eui'; + +import { SectionLoading, TabSettings, TabAliases, TabMappings } from '../shared_imports'; +import { useComponentTemplatesContext } from '../component_templates_context'; +import { TabSummary } from './tab_summary'; +import { ComponentTemplateTabs, TabType } from './tabs'; +import { ManageButton, ManageAction } from './manage_button'; + +interface Props { + componentTemplateName: string; + onClose: () => void; + showFooter?: boolean; + actions?: ManageAction[]; +} + +export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({ + componentTemplateName, + onClose, + actions, +}) => { + const { api } = useComponentTemplatesContext(); + + const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate( + componentTemplateName + ); + + const [activeTab, setActiveTab] = useState('summary'); + + let content: React.ReactNode | undefined; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + color="danger" + iconType="alert" + data-test-subj="sectionError" + > +

{error.message}

+
+ ); + } else if (componentTemplateDetails) { + const { + template: { settings, mappings, aliases }, + } = componentTemplateDetails; + + const tabToComponentMap: Record = { + summary: , + settings: , + mappings: , + aliases: , + }; + + const tabContent = tabToComponentMap[activeTab]; + + content = ( + <> + + + + + {tabContent} + + ); + } + + return ( + + + +

+ {componentTemplateName} +

+
+
+ + {content} + + {actions && ( + + + {/* "Close" link */} + + + + + + + {/* "Manage" context menu */} + {componentTemplateDetails && ( + + + + )} + + + )} +
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/index.ts new file mode 100644 index 0000000000000..11aac200a2f14 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_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 { ComponentTemplateDetailsFlyout } from './component_template_details'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/manage_button.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/manage_button.tsx new file mode 100644 index 0000000000000..c3a4f9b4dce35 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/manage_button.tsx @@ -0,0 +1,105 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + EuiPopover, + EuiButton, + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { ComponentTemplateDeserialized } from '../shared_imports'; + +export interface ManageAction { + name: string; + icon: string; + handleActionClick: () => void; + getIsDisabled?: (data: ComponentTemplateDeserialized) => boolean; + closePopoverOnClick?: boolean; +} + +interface Props { + actions: ManageAction[]; + componentTemplateDetails: ComponentTemplateDeserialized; +} + +export const ManageButton: React.FunctionComponent = ({ + actions, + componentTemplateDetails, +}) => { + const [isPopoverOpen, setIsPopOverOpen] = useState(false); + + const items: EuiContextMenuPanelItemDescriptor[] = actions.map( + ({ name, icon, getIsDisabled, closePopoverOnClick, handleActionClick }) => { + const isDisabled = getIsDisabled ? getIsDisabled(componentTemplateDetails) : false; + + return { + name, + icon, + disabled: isDisabled, + toolTipContent: isDisabled ? ( + + ) : null, + onClick: () => { + handleActionClick(); + + if (closePopoverOnClick) { + setIsPopOverOpen(false); + } + }, + 'data-test-subj': 'action', + }; + } + ); + + return ( + setIsPopOverOpen((prevBoolean) => !prevBoolean)} + > + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopOverOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="rightUp" + repositionOnScroll + > + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx new file mode 100644 index 0000000000000..401186f6c962e --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx @@ -0,0 +1,106 @@ +/* + * 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, + EuiCodeBlock, + EuiTitle, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { ComponentTemplateDeserialized } from '../shared_imports'; + +interface Props { + componentTemplateDetails: ComponentTemplateDeserialized; +} + +export const TabSummary: React.FunctionComponent = ({ componentTemplateDetails }) => { + const { version, _meta, _kbnMeta } = componentTemplateDetails; + + const { usedBy } = _kbnMeta; + const templateIsInUse = usedBy.length > 0; + + return ( + <> + {/* Callout when component template is not in use */} + {!templateIsInUse && ( + <> + + } + iconType="pin" + data-test-subj="notInUseCallout" + size="s" + /> + + + )} + + {/* Summary description list */} + + {/* Used by templates */} + {templateIsInUse && ( + <> + + + + +
    + {usedBy.map((templateName: string) => ( +
  • + + {templateName} + +
  • + ))} +
+
+ + )} + + {/* Version (optional) */} + {version && ( + <> + + + + {version} + + )} + + {/* Metadata (optional) */} + {_meta && ( + <> + + + + + {JSON.stringify(_meta, null, 2)} + + + )} +
+ + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tabs.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tabs.tsx new file mode 100644 index 0000000000000..89370e3f6f1b8 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tabs.tsx @@ -0,0 +1,70 @@ +/* + * 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 { EuiTab, EuiTabs } from '@elastic/eui'; + +export type TabType = 'summary' | 'mappings' | 'aliases' | 'settings'; + +interface Props { + setActiveTab: (id: TabType) => void; + activeTab: TabType; +} + +interface Tab { + id: TabType; + name: string; +} + +const TABS: Tab[] = [ + { + id: 'summary', + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.summaryTabTitle', { + defaultMessage: 'Summary', + }), + }, + { + id: 'settings', + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.settingsTabTitle', { + defaultMessage: 'Settings', + }), + }, + { + id: 'mappings', + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.mappingsTabTitle', { + defaultMessage: 'Mappings', + }), + }, + { + id: 'aliases', + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.aliasesTabTitle', { + defaultMessage: 'Aliases', + }), + }, +]; + +export const ComponentTemplateTabs: React.FunctionComponent = ({ + setActiveTab, + activeTab, +}) => { + return ( + + {TABS.map((tab) => ( + { + setActiveTab(tab.id); + }} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subj={`${tab.id}Tab`} + > + {tab.name} + + ))} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 41fa608ef538b..05a5ed462d8f7 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -5,24 +5,39 @@ */ import React, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ScopedHistory } from 'kibana/public'; -import { SectionLoading } from '../shared_imports'; -import { useComponentTemplatesContext } from '../component_templates_context'; +import { SectionLoading, ComponentTemplateDeserialized } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants'; - +import { useComponentTemplatesContext } from '../component_templates_context'; +import { ComponentTemplateDetailsFlyout } from '../component_template_details'; import { EmptyPrompt } from './empty_prompt'; import { ComponentTable } from './table'; import { LoadError } from './error'; import { ComponentTemplatesDeleteModal } from './delete_modal'; -export const ComponentTemplateList: React.FunctionComponent = () => { +interface Props { + componentTemplateName?: string; + history: RouteComponentProps['history']; +} + +export const ComponentTemplateList: React.FunctionComponent = ({ + componentTemplateName, + history, +}) => { const { api, trackMetric } = useComponentTemplatesContext(); const { data, isLoading, error, sendRequest } = api.useLoadComponentTemplates(); const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]); + const goToList = () => { + return history.push('component_templates'); + }; + // Track component loaded useEffect(() => { trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD); @@ -49,6 +64,7 @@ export const ComponentTemplateList: React.FunctionComponent = () => { componentTemplates={data} onReloadClick={sendRequest} onDeleteClick={setComponentTemplatesToDelete} + history={history as ScopedHistory} /> ); } else if (error) { @@ -58,18 +74,44 @@ export const ComponentTemplateList: React.FunctionComponent = () => { return (
{content} + + {/* delete modal */} {componentTemplatesToDelete?.length > 0 ? ( { if (deleteResponse?.hasDeletedComponentTemplates) { // refetch the component templates sendRequest(); + // go back to list view (if deleted from details flyout) + goToList(); } setComponentTemplatesToDelete([]); }} componentTemplatesToDelete={componentTemplatesToDelete} /> ) : null} + + {/* details flyout */} + {componentTemplateName && ( + + details._kbnMeta.usedBy.length > 0, + closePopoverOnClick: true, + handleActionClick: () => { + setComponentTemplatesToDelete([componentTemplateName]); + }, + }, + ]} + /> + )}
); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx index af8ab1b94c790..d0636da8cf93a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list_container.tsx @@ -5,16 +5,28 @@ */ import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import { ComponentTemplatesAuthProvider } from './auth_provider'; import { ComponentTemplatesWithPrivileges } from './with_privileges'; import { ComponentTemplateList } from './component_template_list'; -export const ComponentTemplateListContainer: React.FunctionComponent = () => { +interface MatchParams { + componentTemplateName?: string; +} + +export const ComponentTemplateListContainer: React.FunctionComponent> = ({ + match: { + params: { componentTemplateName }, + }, + history, +}) => { return ( - + ); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index 2d9557e64e6e7..b67a249ae6976 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -12,21 +12,30 @@ import { EuiInMemoryTableProps, EuiTextColor, EuiIcon, + EuiLink, } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; -import { ComponentTemplateListItem } from '../types'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ComponentTemplateListItem } from '../shared_imports'; +import { UIM_COMPONENT_TEMPLATE_DETAILS } from '../constants'; +import { useComponentTemplatesContext } from '../component_templates_context'; export interface Props { componentTemplates: ComponentTemplateListItem[]; onReloadClick: () => void; onDeleteClick: (componentTemplateName: string[]) => void; + history: ScopedHistory; } export const ComponentTable: FunctionComponent = ({ componentTemplates, onReloadClick, onDeleteClick, + history, }) => { + const { trackMetric } = useComponentTemplatesContext(); + const [selection, setSelection] = useState([]); const tableProps: EuiInMemoryTableProps = { @@ -120,6 +129,21 @@ export const ComponentTable: FunctionComponent = ({ defaultMessage: 'Name', }), sortable: true, + render: (name: string) => ( + /* eslint-disable-next-line @elastic/eui/href-or-on-click */ + trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS) + )} + data-test-subj="templateDetailsLink" + > + {name} + + ), }, { field: 'usedBy', diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx index 55f20ce21d417..e8116409def4b 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx @@ -14,7 +14,6 @@ const ComponentTemplatesContext = createContext(undefined); interface Props { httpClient: HttpSetup; apiBasePath: string; - appBasePath: string; trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; docLinks: DocLinksSetup; toasts: NotificationsSetup['toasts']; @@ -27,7 +26,6 @@ interface Context { documentation: ReturnType; trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; toasts: NotificationsSetup['toasts']; - appBasePath: string; } export const ComponentTemplatesProvider = ({ @@ -37,7 +35,7 @@ export const ComponentTemplatesProvider = ({ value: Props; children: React.ReactNode; }) => { - const { httpClient, apiBasePath, trackMetric, docLinks, toasts, appBasePath } = value; + const { httpClient, apiBasePath, trackMetric, docLinks, toasts } = value; const useRequest = getUseRequest(httpClient); const sendRequest = getSendRequest(httpClient); @@ -47,7 +45,7 @@ export const ComponentTemplatesProvider = ({ return ( {children} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts index 501acde07fc00..e9acfa8dcc56d 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts @@ -8,6 +8,7 @@ export const UIM_COMPONENT_TEMPLATE_LIST_LOAD = 'component_template_list_load'; export const UIM_COMPONENT_TEMPLATE_DELETE = 'component_template_delete'; export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many'; +export const UIM_COMPONENT_TEMPLATE_DETAILS = 'component_template_details'; // privileges export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates']; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts index e0219ec71787f..72e79a57ae413 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts @@ -8,4 +8,4 @@ export { ComponentTemplatesProvider } from './component_templates_context'; export { ComponentTemplateList } from './component_template_list'; -export * from './types'; +export { ComponentTemplateDetailsFlyout } from './component_template_details'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 351e83c6c0cb5..4a8cf965adfb9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ComponentTemplateListItem } from '../types'; -import { UseRequestHook, SendRequestHook } from './request'; +import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, UIM_COMPONENT_TEMPLATE_DELETE } from '../constants'; +import { UseRequestHook, SendRequestHook } from './request'; export const getApi = ( useRequest: UseRequestHook, @@ -37,8 +37,16 @@ export const getApi = ( return result; } + function useLoadComponentTemplate(name: string) { + return useRequest({ + path: `${apiBasePath}/component_templates/${encodeURIComponent(name)}`, + method: 'get', + }); + } + return { useLoadComponentTemplates, deleteComponentTemplates, + useLoadComponentTemplate, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index 049204f03c0c1..4e56f4a8c9818 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -19,3 +19,11 @@ export { useAuthorizationContext, NotAuthorizedSection, } from '../../../../../../../src/plugins/es_ui_shared/public'; + +export { TabMappings, TabSettings, TabAliases } from '../shared'; + +export { + ComponentTemplateSerialized, + ComponentTemplateDeserialized, + ComponentTemplateListItem, +} from '../../../../common'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/types.ts b/x-pack/plugins/index_management/public/application/components/component_templates/types.ts deleted file mode 100644 index 0aab3b6b0a94a..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/component_templates/types.ts +++ /dev/null @@ -1,17 +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. - */ - -// Ideally, we shouldn't depend on anything in index management that is -// outside of the components_templates directory -// We could consider creating shared types or duplicating the types here if -// the component_templates app were to move outside of index management -import { - ComponentTemplateSerialized, - ComponentTemplateDeserialized, - ComponentTemplateListItem, -} from '../../../../common'; - -export { ComponentTemplateSerialized, ComponentTemplateDeserialized, ComponentTemplateListItem }; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/index.ts new file mode 100644 index 0000000000000..af5cd500a286a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TabAliases } from './tab_aliases'; +export { TabMappings } from './tab_mappings'; +export { TabSettings } from './tab_settings'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx new file mode 100644 index 0000000000000..bcf59fd13bad6 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_aliases.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; + +import { Aliases } from '../../../../../../common'; + +interface Props { + aliases: Aliases | undefined; +} + +export const TabAliases: React.FunctionComponent = ({ aliases }) => { + if (aliases && Object.keys(aliases).length) { + return ( +
+ {JSON.stringify(aliases, null, 2)} +
+ ); + } + + return ( + + } + iconType="pin" + data-test-subj="noAliasesCallout" + size="s" + /> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx new file mode 100644 index 0000000000000..3c3d88345e5a1 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_mappings.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; +import { Mappings } from '../../../../../../common'; + +interface Props { + mappings: Mappings | undefined; +} + +export const TabMappings: React.FunctionComponent = ({ mappings }) => { + if (mappings && Object.keys(mappings).length) { + return ( +
+ {JSON.stringify(mappings, null, 2)} +
+ ); + } + + return ( + + } + iconType="pin" + data-test-subj="noMappingsCallout" + size="s" + /> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx new file mode 100644 index 0000000000000..6d5cf09588a1c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/details_panel/tab_settings.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; +import { IndexSettings } from '../../../../../../common'; + +interface Props { + settings: IndexSettings | undefined; +} + +export const TabSettings: React.FunctionComponent = ({ settings }) => { + if (settings && Object.keys(settings).length) { + return ( +
+ {JSON.stringify(settings, null, 2)} +
+ ); + } + + return ( + + } + iconType="pin" + data-test-subj="noSettingsCallout" + size="s" + /> + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts new file mode 100644 index 0000000000000..90d66bd1a5da4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/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 { TabAliases, TabMappings, TabSettings } from './details_panel'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/index.ts b/x-pack/plugins/index_management/public/application/components/shared/index.ts new file mode 100644 index 0000000000000..e015ef72e244f --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/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 { TabAliases, TabMappings, TabSettings } from './components'; diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx index 5d1096c9ee24e..ff54b4b1bfe35 100644 --- a/x-pack/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -10,7 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { CoreStart } from '../../../../../src/core/public'; -import { API_BASE_PATH, BASE_PATH } from '../../common'; +import { API_BASE_PATH } from '../../common'; import { AppContextProvider, AppDependencies } from './app_context'; import { App } from './app'; @@ -32,7 +32,6 @@ export const renderApp = ( const componentTemplateProviderValues = { httpClient: services.httpService.httpClient, apiBasePath: API_BASE_PATH, - appBasePath: BASE_PATH, trackMetric: services.uiMetricService.trackMetric.bind(services.uiMetricService), docLinks, toasts: notifications.toasts, diff --git a/x-pack/plugins/index_management/public/application/sections/home/home.tsx b/x-pack/plugins/index_management/public/application/sections/home/home.tsx index 51deaf42cc72c..7bd04cdbf0c91 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/home.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/home.tsx @@ -150,7 +150,14 @@ export const IndexManagementHome: React.FunctionComponent - +