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' }, }); })