diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts
index e1251f03d044b..2c79e3b3ee8ff 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts
@@ -18,16 +18,11 @@ import {
import { HttpError, Status } from '../../../../../../../common/types/api';
import { MlInferencePipeline } from '../../../../../../../common/types/pipelines';
-import { generateEncodedPath } from '../../../../../shared/encode_path_params';
import { getErrorsFromHttpResponse } from '../../../../../shared/flash_messages/handle_api_errors';
-import { KibanaLogic } from '../../../../../shared/kibana';
import { MappingsApiLogic } from '../../../../api/mappings/mappings_logic';
import { CreateMlInferencePipelineApiLogic } from '../../../../api/ml_models/create_ml_inference_pipeline';
import { MLModelsApiLogic } from '../../../../api/ml_models/ml_models_logic';
-import { SEARCH_INDEX_TAB_PATH } from '../../../../routes';
-import { SearchIndexTabId } from '../../search_index';
-
import { AddInferencePipelineFormErrors, InferencePipelineConfiguration } from './types';
import {
isSupportedMLModel,
@@ -126,14 +121,6 @@ export const MLInferenceLogic = kea<
},
events: {},
listeners: ({ values, actions }) => ({
- createApiSuccess: () => {
- KibanaLogic.values.navigateToUrl(
- generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
- indexName: values.addInferencePipelineModal.indexName,
- tabId: SearchIndexTabId.PIPELINES,
- })
- );
- },
createPipeline: () => {
const {
addInferencePipelineModal: { configuration, indexName },
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipeline_json_badges.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipeline_json_badges.tsx
new file mode 100644
index 0000000000000..9a690ab437dab
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipeline_json_badges.tsx
@@ -0,0 +1,147 @@
+/*
+ * 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 React from 'react';
+
+import { useValues } from 'kea';
+
+import { EuiBadgeGroup, EuiBadge, EuiToolTip } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+import { DEFAULT_PIPELINE_NAME } from '../../../../../../common/constants';
+
+import { isManagedPipeline } from '../../../../shared/pipelines/is_managed';
+
+import { IndexPipelinesConfigurationsLogic } from './pipelines_json_configurations_logic';
+
+const ManagedPipelineBadge: React.FC = () => (
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.managed',
+ { defaultMessage: 'Managed' }
+ )}
+
+
+);
+
+const UnmanagedPipelineBadge: React.FC = () => (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.ingestPipelines',
+ { defaultMessage: 'Ingest Pipelines' }
+ )}
+
+ ),
+ }}
+ />
+ }
+ >
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.unmanaged',
+ { defaultMessage: 'Unmanaged' }
+ )}
+
+
+);
+
+const SharedPipelineBadge: React.FC = () => (
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.shared',
+ { defaultMessage: 'Shared' }
+ )}
+
+
+);
+
+const IndexPipelineBadge: React.FC = () => (
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.indexSpecific',
+ { defaultMessage: 'Index specific' }
+ )}
+
+
+);
+
+const MlInferenceBadge: React.FC = () => (
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.mlInference',
+ { defaultMessage: 'ML Inference' }
+ )}
+
+
+);
+
+export const PipelineJSONBadges: React.FC = () => {
+ const {
+ indexName,
+ selectedPipeline: pipeline,
+ selectedPipelineId: pipelineName,
+ } = useValues(IndexPipelinesConfigurationsLogic);
+ if (!pipeline) {
+ return <>>;
+ }
+ const badges: JSX.Element[] = [];
+ if (isManagedPipeline(pipeline)) {
+ badges.push();
+ } else {
+ badges.push();
+ }
+ if (pipelineName === DEFAULT_PIPELINE_NAME) {
+ badges.push();
+ }
+ if (pipelineName?.endsWith('@ml-inference')) {
+ badges.push();
+ } else if (pipelineName?.includes(indexName)) {
+ badges.push();
+ }
+ return {badges};
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
index 9cab24190a2de..f695b7c541c5a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
@@ -9,7 +9,15 @@ import React from 'react';
import { useActions, useValues } from 'kea';
-import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+ EuiPanel,
+ EuiSpacer,
+ EuiTabbedContent,
+ EuiTabbedContentTab,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -21,6 +29,7 @@ import { IngestPipelinesCard } from './ingest_pipelines_card';
import { AddMLInferencePipelineButton } from './ml_inference/add_ml_inference_button';
import { AddMLInferencePipelineModal } from './ml_inference/add_ml_inference_pipeline_modal';
import { MlInferencePipelineProcessorsCard } from './ml_inference_pipeline_processors_card';
+import { PipelinesJSONConfigurations } from './pipelines_json_configurations';
import { PipelinesLogic } from './pipelines_logic';
export const SearchIndexPipelines: React.FC = () => {
@@ -34,6 +43,19 @@ export const SearchIndexPipelines: React.FC = () => {
useActions(PipelinesLogic);
const apiIndex = isApiIndex(index);
+ const pipelinesTabs: EuiTabbedContentTab[] = [
+ {
+ content: ,
+ id: 'json-configurations',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations',
+ {
+ defaultMessage: 'JSON configurations',
+ }
+ ),
+ },
+ ];
+
return (
<>
@@ -82,8 +104,7 @@ export const SearchIndexPipelines: React.FC = () => {
>
-
-
+
{
+
+
+
+
+
{showAddMlInferencePipelineModal && (
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_json_configurations.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_json_configurations.tsx
new file mode 100644
index 0000000000000..3cba142347fc3
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_json_configurations.tsx
@@ -0,0 +1,142 @@
+/*
+ * 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 React from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import {
+ EuiButtonEmpty,
+ EuiCodeBlock,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiLink,
+ EuiNotificationBadge,
+ EuiSelect,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { DataPanel } from '../../../../shared/data_panel/data_panel';
+import { docLinks } from '../../../../shared/doc_links';
+import { HttpLogic } from '../../../../shared/http';
+import { isManagedPipeline } from '../../../../shared/pipelines/is_managed';
+
+import { PipelineJSONBadges } from './pipeline_json_badges';
+import { IndexPipelinesConfigurationsLogic } from './pipelines_json_configurations_logic';
+
+export const PipelinesJSONConfigurations: React.FC = () => {
+ const { http } = useValues(HttpLogic);
+ const { pipelineNames, selectedPipeline, selectedPipelineId, selectedPipelineJSON } = useValues(
+ IndexPipelinesConfigurationsLogic
+ );
+ const { selectPipeline } = useActions(IndexPipelinesConfigurationsLogic);
+ return (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.title',
+ { defaultMessage: 'Pipeline configurations' }
+ )}
+
+ }
+ subtitle={i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.subtitle',
+ { defaultMessage: 'View the JSON for your pipeline configurations on this index.' }
+ )}
+ footerDocLink={
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.ingestionPipelines.docLink',
+ {
+ defaultMessage: 'Learn more about how Enterprise Search uses ingest pipelines',
+ }
+ )}
+
+ }
+ action={
+ pipelineNames.length > 0 && (
+ {pipelineNames.length}
+ )
+ }
+ iconType="visVega"
+ >
+
+ ({ text: name, value: name }))}
+ value={selectedPipelineId}
+ onChange={(e) => selectPipeline(e.target.value)}
+ />
+
+
+ {selectedPipeline && (
+ <>
+
+
+
+
+
+ {isManagedPipeline(selectedPipeline) ? (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.action.view',
+ {
+ defaultMessage: 'View in Stack Management',
+ }
+ )}
+
+ ) : (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.tabs.jsonConfigurations.action.edit',
+ {
+ defaultMessage: 'Edit in Stack Management',
+ }
+ )}
+
+ )}
+
+
+
+
+ {selectedPipelineJSON}
+
+ >
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_json_configurations_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_json_configurations_logic.ts
new file mode 100644
index 0000000000000..165cdfc99eb8b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_json_configurations_logic.ts
@@ -0,0 +1,102 @@
+/*
+ * 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 { kea, MakeLogicType } from 'kea';
+
+import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
+
+import { Actions } from '../../../../shared/api_logic/create_api_logic';
+import {
+ FetchCustomPipelineApiLogicArgs,
+ FetchCustomPipelineApiLogicResponse,
+ FetchCustomPipelineApiLogic,
+} from '../../../api/index/fetch_custom_pipeline_api_logic';
+import { IndexNameLogic } from '../index_name_logic';
+
+interface IndexPipelinesConfigurationsActions {
+ fetchIndexPipelinesDataSuccess: Actions<
+ FetchCustomPipelineApiLogicArgs,
+ FetchCustomPipelineApiLogicResponse
+ >['apiSuccess'];
+ selectPipeline: (pipeline: string) => { pipeline: string };
+}
+
+interface IndexPipelinesConfigurationsValues {
+ indexName: string;
+ indexPipelinesData: FetchCustomPipelineApiLogicResponse;
+ pipelineNames: string[];
+ pipelines: Record;
+ selectedPipeline: IngestPipeline | undefined;
+ selectedPipelineId: string;
+ selectedPipelineJSON: string;
+}
+
+export const IndexPipelinesConfigurationsLogic = kea<
+ MakeLogicType
+>({
+ actions: {
+ selectPipeline: (pipeline: string) => ({ pipeline }),
+ },
+ connect: {
+ actions: [FetchCustomPipelineApiLogic, ['apiSuccess as fetchIndexPipelinesDataSuccess']],
+ values: [
+ IndexNameLogic,
+ ['indexName'],
+ FetchCustomPipelineApiLogic,
+ ['data as indexPipelinesData'],
+ ],
+ },
+ listeners: ({ actions, values }) => ({
+ fetchIndexPipelinesDataSuccess: (pipelines) => {
+ const names = Object.keys(pipelines ?? {}).sort();
+ if (names.length > 0 && values.selectedPipelineId.length === 0) {
+ const defaultPipeline = names.includes(values.indexName) ? values.indexName : names[0];
+ actions.selectPipeline(defaultPipeline);
+ }
+ },
+ }),
+ reducers: () => ({
+ selectedPipelineId: [
+ '',
+ {
+ selectPipeline: (_, { pipeline }) => pipeline,
+ },
+ ],
+ }),
+ selectors: ({ selectors }) => ({
+ pipelines: [
+ () => [selectors.indexPipelinesData],
+ (indexPipelines: FetchCustomPipelineApiLogicResponse) => {
+ return indexPipelines ?? {};
+ },
+ ],
+ pipelineNames: [
+ () => [selectors.pipelines],
+ (pipelines: Record) => {
+ return Object.keys(pipelines).sort();
+ },
+ ],
+ selectedPipeline: [
+ () => [selectors.selectedPipelineId, selectors.pipelines],
+ (selectedPipelineId: string, pipelines: Record) => {
+ if (pipelines.hasOwnProperty(selectedPipelineId)) {
+ return pipelines[selectedPipelineId];
+ }
+ return undefined;
+ },
+ ],
+ selectedPipelineJSON: [
+ () => [selectors.selectedPipeline],
+ (selectedPipeline: IngestPipeline | undefined) => {
+ if (selectedPipeline) {
+ return JSON.stringify(selectedPipeline, null, 2);
+ }
+ return '';
+ },
+ ],
+ }),
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts
index 99d241507dd2a..bfcf297309d69 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts
@@ -212,7 +212,10 @@ export const PipelinesLogic = kea {
+ // Re-fetch processors to ensure we display newly added ml processor
actions.fetchMlInferenceProcessors({ indexName: values.index.name });
+ // Needed to ensure correct JSON is available in the JSON configurations tab
+ actions.fetchCustomPipeline({ indexName: values.index.name });
},
deleteMlPipelineError: (error) => flashAPIErrors(error),
deleteMlPipelineSuccess: (value) => {
@@ -229,7 +232,10 @@ export const PipelinesLogic = kea {
if (!values.showModal) {
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/pipelines/is_managed.ts b/x-pack/plugins/enterprise_search/public/applications/shared/pipelines/is_managed.ts
new file mode 100644
index 0000000000000..30cf5ac145c87
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/pipelines/is_managed.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
+
+interface IngestPipelineWithMetadata extends IngestPipeline {
+ _meta: {
+ managed?: boolean;
+ managed_by?: string;
+ };
+}
+
+const isIngestPipelineWithMetadata = (
+ pipeline: IngestPipeline
+): pipeline is IngestPipelineWithMetadata => {
+ return pipeline.hasOwnProperty('_meta');
+};
+
+export const isManagedPipeline = (pipeline: IngestPipeline): boolean => {
+ if (isIngestPipelineWithMetadata(pipeline)) {
+ return Boolean(pipeline._meta.managed);
+ }
+ return false;
+};
diff --git a/x-pack/plugins/enterprise_search/server/lib/pipelines/get_pipeline.ts b/x-pack/plugins/enterprise_search/server/lib/pipelines/get_pipeline.ts
new file mode 100644
index 0000000000000..a02b4cdd8b19b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/pipelines/get_pipeline.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { IngestGetPipelineResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { IScopedClusterClient } from '@kbn/core/server';
+
+export const getPipeline = async (
+ pipelineName: string,
+ client: IScopedClusterClient
+): Promise => {
+ try {
+ const pipelinesResponse = await client.asCurrentUser.ingest.getPipeline({
+ id: pipelineName,
+ });
+
+ return pipelinesResponse;
+ } catch (error) {
+ // If we can't find anything, we return an empty object
+ return {};
+ }
+};
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts
index db46da11f5f57..ef6f8131ee2c1 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts
@@ -13,6 +13,7 @@ import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
+import { DEFAULT_PIPELINE_NAME } from '../../../common/constants';
import { ErrorCode } from '../../../common/types/error_codes';
import { deleteConnectorById } from '../../lib/connectors/delete_connector';
@@ -28,6 +29,7 @@ import { fetchMlInferencePipelineProcessors } from '../../lib/indices/fetch_ml_i
import { generateApiKey } from '../../lib/indices/generate_api_key';
import { createIndexPipelineDefinitions } from '../../lib/pipelines/create_pipeline_definitions';
import { getCustomPipelines } from '../../lib/pipelines/get_custom_pipelines';
+import { getPipeline } from '../../lib/pipelines/get_pipeline';
import { RouteDependencies } from '../../plugin';
import { createError } from '../../utils/create_error';
import {
@@ -293,9 +295,15 @@ export function registerIndexRoutes({
elasticsearchErrorHandler(log, async (context, request, response) => {
const indexName = decodeURIComponent(request.params.indexName);
const { client } = (await context.core).elasticsearch;
- const pipelines = await getCustomPipelines(indexName, client);
+ const [defaultPipeline, customPipelines] = await Promise.all([
+ getPipeline(DEFAULT_PIPELINE_NAME, client),
+ getCustomPipelines(indexName, client),
+ ]);
return response.ok({
- body: pipelines,
+ body: {
+ ...defaultPipeline,
+ ...customPipelines,
+ },
headers: { 'content-type': 'application/json' },
});
})