diff --git a/x-pack/platform/plugins/shared/index_management/public/application/constants/index.ts b/x-pack/platform/plugins/shared/index_management/public/application/constants/index.ts index f72e56310a9c7..04b30a168e983 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/constants/index.ts +++ b/x-pack/platform/plugins/shared/index_management/public/application/constants/index.ts @@ -10,3 +10,5 @@ export const REACT_ROOT_ID = 'indexManagementReactRoot'; export const ENRICH_POLICIES_REQUIRED_PRIVILEGES = ['manage_enrich']; export * from './ilm_locator'; + +export * from './ingest_pipelines_locator'; diff --git a/x-pack/platform/plugins/shared/index_management/public/application/constants/ingest_pipelines_locator.ts b/x-pack/platform/plugins/shared/index_management/public/application/constants/ingest_pipelines_locator.ts new file mode 100644 index 0000000000000..5d6037d2e3472 --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/public/application/constants/ingest_pipelines_locator.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const INGEST_PIPELINES_LOCATOR_ID = 'INGEST_PIPELINES_APP_LOCATOR'; +export const INGEST_PIPELINES_EDIT = 'pipeline_edit'; +export const INGEST_PIPELINES_LIST = 'pipelines_list'; diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx index a6fbe93642813..83c60f3d04220 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx @@ -25,8 +25,9 @@ import { useAppContext } from '../../../../../app_context'; import { serializeAsESLifecycle } from '../../../../../../../common/lib'; import { getLifecycleValue } from '../../../../../lib/data_streams'; import { TemplateDeserialized } from '../../../../../../../common'; -import { ILM_PAGES_POLICY_EDIT } from '../../../../../constants'; +import { ILM_PAGES_POLICY_EDIT, INGEST_PIPELINES_EDIT } from '../../../../../constants'; import { useIlmLocator } from '../../../../../services/use_ilm_locator'; +import { useIngestPipelinesLocator } from '../../../../../services/use_ingest_pipeline_locator'; import { allowAutoCreateRadioIds } from '../../../../../../../common/constants'; import { indexModeLabels } from '../../../../../lib/index_mode_labels'; @@ -66,6 +67,13 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails }) const { history, core } = useAppContext(); const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, ilmPolicy?.name); + // Compute the linked ingest pipeline URL + const linkedIngestPipeline = templateDetails?.template?.settings?.index?.default_pipeline; + const linkedIngestPipelineUrl = useIngestPipelinesLocator( + INGEST_PIPELINES_EDIT, + linkedIngestPipeline + ); + return ( <> @@ -154,6 +162,25 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails }) )} + + {linkedIngestPipeline && ( + <> + + + + + core.application.navigateToUrl(linkedIngestPipelineUrl)} + data-test-subj="linkedIngestPipeline" + > + {linkedIngestPipeline} + + + + )} diff --git a/x-pack/platform/plugins/shared/index_management/public/application/services/use_ingest_pipeline_locator.ts b/x-pack/platform/plugins/shared/index_management/public/application/services/use_ingest_pipeline_locator.ts new file mode 100644 index 0000000000000..cfcd9ff326f62 --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/public/application/services/use_ingest_pipeline_locator.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useLocatorUrl } from '@kbn/share-plugin/public'; +import { useAppContext } from '../app_context'; +import { + INGEST_PIPELINES_LOCATOR_ID, + INGEST_PIPELINES_EDIT, + INGEST_PIPELINES_LIST, +} from '../constants'; + +export const useIngestPipelinesLocator = ( + page: typeof INGEST_PIPELINES_EDIT | typeof INGEST_PIPELINES_LIST, + pipelineId?: string +): string => { + const ctx = useAppContext(); + const locator = + pipelineId === undefined ? null : ctx.url.locators.get(INGEST_PIPELINES_LOCATOR_ID)!; + const url = useLocatorUrl(locator, { page, pipelineId }, {}, [page, pipelineId]); + + return url; +}; diff --git a/x-pack/test/functional/apps/index_management/index_templates_tab/index_template_tab.ts b/x-pack/test/functional/apps/index_management/index_templates_tab/index_template_tab.ts index 97ce2233e6546..003e7e38c00a5 100644 --- a/x-pack/test/functional/apps/index_management/index_templates_tab/index_template_tab.ts +++ b/x-pack/test/functional/apps/index_management/index_templates_tab/index_template_tab.ts @@ -14,6 +14,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const security = getService('security'); const testSubjects = getService('testSubjects'); const es = getService('es'); + const browser = getService('browser'); const INDEX_TEMPLATE_NAME = 'index-template-test-name'; @@ -28,10 +29,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); afterEach(async () => { - await es.indices.deleteIndexTemplate({ - name: INDEX_TEMPLATE_NAME, - }); - await testSubjects.click('reloadButton'); + await es.indices.deleteIndexTemplate( + { + name: INDEX_TEMPLATE_NAME, + }, + { ignore: [404] } + ); + + if (await testSubjects.exists('reloadButton')) { + await testSubjects.click('reloadButton'); + } }); describe('index template creation', () => { @@ -221,5 +228,48 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + describe('Index template list', () => { + const TEST_TEMPLATE = 'a_test_template'; + const INDEX_PATTERN = `index_pattern_${Math.random()}`; + + before(async () => { + await es.indices.putIndexTemplate({ + name: TEST_TEMPLATE, + body: { + index_patterns: [INDEX_PATTERN], + template: { + settings: { + default_pipeline: 'test_pipeline', + }, + }, + }, + }); + + // Navigate to the index management + await pageObjects.common.navigateToApp('indexManagement'); + // Navigate to the templates tab + await pageObjects.indexManagement.changeTabs('templatesTab'); + }); + + after(async () => { + await es.indices.deleteIndexTemplate({ name: TEST_TEMPLATE }, { ignore: [404] }); + }); + + it('shows link to ingest pipeline when default pipeline is set', async () => { + // Open details flyout + await pageObjects.indexManagement.clickIndexTemplate(TEST_TEMPLATE); + + // Click on the linked ingest pipeline button + const linkedPipelineButton = await testSubjects.find('linkedIngestPipeline'); + await linkedPipelineButton.click(); + + // Expect to navigate to the ingest pipeline page + await pageObjects.header.waitUntilLoadingHasFinished(); + // We should've now navigated to the ingest pipelines app + const currentUrl = await browser.getCurrentUrl(); + expect(currentUrl).to.contain('/ingest/ingest_pipelines/edit/test_pipeline'); + }); + }); }); }; diff --git a/x-pack/test/functional/page_objects/index_management_page.ts b/x-pack/test/functional/page_objects/index_management_page.ts index a29e84e677cbc..5b9da3bd54906 100644 --- a/x-pack/test/functional/page_objects/index_management_page.ts +++ b/x-pack/test/functional/page_objects/index_management_page.ts @@ -38,6 +38,17 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext) await policyDetailsLinks[indexOfRow].click(); }, + async clickIndexTemplate(name: string): Promise { + const indexTemplateLinks = await testSubjects.findAll('templateDetailsLink'); + + for (const link of indexTemplateLinks) { + if ((await link.getVisibleText()).includes(name)) { + await link.click(); + return; + } + } + }, + async clickBulkEditDataRetention(dataStreamNames: string[]): Promise { for (const dsName of dataStreamNames) { const checkbox = await testSubjects.find(`checkboxSelectRow-${dsName}`);