diff --git a/common/interfaces.ts b/common/interfaces.ts index baa08165..f9bda443 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -555,3 +555,18 @@ export type SimulateIngestPipelineResponse = { }; export type SearchHit = SimulateIngestPipelineDoc; + +export type IndexResponse = { + indexName: string; + indexDetails: IndexConfiguration; +}; + +export type IngestPipelineResponse = { + pipelineId: string; + ingestPipelineDetails: IngestPipelineConfig; +}; + +export type SearchPipelineResponse = { + pipelineId: string; + searchPipelineDetails: SearchPipelineConfig; +}; diff --git a/public/pages/workflow_detail/tools/resources/resource_list_with_flyout.tsx b/public/pages/workflow_detail/tools/resources/resource_list_with_flyout.tsx index 7c28e707..7b057c04 100644 --- a/public/pages/workflow_detail/tools/resources/resource_list_with_flyout.tsx +++ b/public/pages/workflow_detail/tools/resources/resource_list_with_flyout.tsx @@ -24,9 +24,18 @@ import { WorkflowResource, customStringify, } from '../../../../../common'; -import { fetchResourceData } from '../../../../utils'; -import { AppState, useAppDispatch } from '../../../../store'; -import { getDataSourceId } from '../../../../utils'; +import { + AppState, + useAppDispatch, + getIndex, + getIngestPipeline, + getSearchPipeline, +} from '../../../../store'; +import { + extractIdsByStepType, + getDataSourceId, + getErrorMessageForStepType, +} from '../../../../utils'; import { columns } from './columns'; import { useSelector } from 'react-redux'; @@ -42,7 +51,16 @@ export function ResourceListWithFlyout(props: ResourceListFlyoutProps) { const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); const [resourceDetails, setResourceDetails] = useState(null); - const loading = useSelector((state: AppState) => state.opensearch.loading); + const [rowErrorMessage, setRowErrorMessage] = useState(null); + const { + loading, + getIndexErrorMessage, + getIngestPipelineErrorMessage, + getSearchPipelineErrorMessage, + indexDetails, + ingestPipelineDetails, + searchPipelineDetails, + } = useSelector((state: AppState) => state.opensearch); // Hook to initialize all resources. Reduce to unique IDs, since // the backend resources may include the same resource multiple times @@ -57,6 +75,36 @@ export function ResourceListWithFlyout(props: ResourceListFlyoutProps) { } }, [props.workflow?.resourcesCreated]); + useEffect(() => { + const { + indexIds, + ingestPipelineIds, + searchPipelineIds, + } = extractIdsByStepType(allResources); + + if (indexIds) { + try { + dispatch(getIndex({ index: indexIds, dataSourceId })); + } catch {} + } + + if (ingestPipelineIds) { + try { + dispatch( + getIngestPipeline({ pipelineId: ingestPipelineIds, dataSourceId }) + ); + } catch {} + } + + if (searchPipelineIds) { + try { + dispatch( + getSearchPipeline({ pipelineId: searchPipelineIds, dataSourceId }) + ); + } catch {} + } + }, [allResources]); + const sorting = { sort: { field: 'id', @@ -73,10 +121,19 @@ export function ResourceListWithFlyout(props: ResourceListFlyoutProps) { const openFlyout = async (row: WorkflowResource) => { setSelectedRowData(row); setIsFlyoutVisible(true); - try { - const result = await fetchResourceData(row, dataSourceId!, dispatch); - setResourceDetails(customStringify(result)); - } catch (error) {} + const value = + indexDetails[row.id] ?? + ingestPipelineDetails[row.id] ?? + searchPipelineDetails[row.id] ?? + ''; + setResourceDetails(customStringify({ [row.id]: value })); + const resourceDetailsErrorMessage = getErrorMessageForStepType( + row.stepType, + getIndexErrorMessage, + getIngestPipelineErrorMessage, + getSearchPipelineErrorMessage + ); + setRowErrorMessage(resourceDetailsErrorMessage); }; // Closes the flyout and resets the selected resource data. @@ -130,7 +187,7 @@ export function ResourceListWithFlyout(props: ResourceListFlyoutProps) { - {resourceDetails && !loading ? ( + {!rowErrorMessage && !loading ? ( {resourceDetails} - ) : ( + ) : loading ? ( } title={

Loading

} /> + ) : ( + Error loading resource details} + body={

{rowErrorMessage}

} + /> )}
diff --git a/public/pages/workflows/workflow_list/resource_list.tsx b/public/pages/workflows/workflow_list/resource_list.tsx index 3848033d..cac482b1 100644 --- a/public/pages/workflows/workflow_list/resource_list.tsx +++ b/public/pages/workflows/workflow_list/resource_list.tsx @@ -22,10 +22,19 @@ import { WorkflowResource, customStringify, } from '../../../../common'; -import { AppState, useAppDispatch } from '../../../store'; -import { fetchResourceData, getDataSourceId } from '../../../utils'; +import { + AppState, + getIndex, + getIngestPipeline, + getSearchPipeline, + useAppDispatch, +} from '../../../store'; +import { + extractIdsByStepType, + getDataSourceId, + getErrorMessageForStepType, +} from '../../../utils'; import { useSelector } from 'react-redux'; -import { isEmpty } from 'lodash'; interface ResourceListProps { workflow?: Workflow; @@ -41,7 +50,15 @@ export function ResourceList(props: ResourceListProps) { const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{ [key: string]: any; }>({}); - const loading = useSelector((state: AppState) => state.opensearch.loading); + const { + loading, + getIndexErrorMessage, + getIngestPipelineErrorMessage, + getSearchPipelineErrorMessage, + indexDetails, + ingestPipelineDetails, + searchPipelineDetails, + } = useSelector((state: AppState) => state.opensearch); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const [sortField, setSortField] = useState('id'); @@ -60,9 +77,39 @@ export function ResourceList(props: ResourceListProps) { } }, [props.workflow?.resourcesCreated]); + useEffect(() => { + const { + indexIds, + ingestPipelineIds, + searchPipelineIds, + } = extractIdsByStepType(allResources); + + if (indexIds) { + try { + dispatch(getIndex({ index: indexIds, dataSourceId })); + } catch {} + } + + if (ingestPipelineIds) { + try { + dispatch( + getIngestPipeline({ pipelineId: ingestPipelineIds, dataSourceId }) + ); + } catch {} + } + + if (searchPipelineIds) { + try { + dispatch( + getSearchPipeline({ pipelineId: searchPipelineIds, dataSourceId }) + ); + } catch {} + } + }, [allResources]); + // Renders the expanded row to show resource details in a code block. const renderExpandedRow = useCallback( - (data: any) => ( + (data: any, resourceDetailsErrorMessage?: string) => ( @@ -70,7 +117,7 @@ export function ResourceList(props: ResourceListProps) { - {!data.startsWith('error:') && !loading ? ( + {!resourceDetailsErrorMessage && !loading ? ( Error loading resource details} - body={

{data.replace(/^error:\s*/, '')}

} + body={

{resourceDetailsErrorMessage}

} /> )}
@@ -106,18 +153,23 @@ export function ResourceList(props: ResourceListProps) { delete updatedItemIdToExpandedRowMap[item.id]; setItemIdToExpandedRowMap(updatedItemIdToExpandedRowMap); } else { - try { - const result = await fetchResourceData(item, dataSourceId!, dispatch); - if (result) { - setItemIdToExpandedRowMap((prevMap) => ({ - ...prevMap, - [item.id]: renderExpandedRow(result), - })); - } - } catch (error) { + const value = + indexDetails[item.id] ?? + ingestPipelineDetails[item.id] ?? + searchPipelineDetails[item.id] ?? + ''; + const resourceDetailsErrorMessage = getErrorMessageForStepType( + item.stepType, + getIndexErrorMessage, + getIngestPipelineErrorMessage, + getSearchPipelineErrorMessage + ); + + const result = { [item.id]: value }; + if (result) { setItemIdToExpandedRowMap((prevMap) => ({ ...prevMap, - [item.id]: renderExpandedRow('error:' + error), + [item.id]: renderExpandedRow(result, resourceDetailsErrorMessage), })); } } diff --git a/public/store/reducers/opensearch_reducer.ts b/public/store/reducers/opensearch_reducer.ts index 0e00ac1c..75d905eb 100644 --- a/public/store/reducers/opensearch_reducer.ts +++ b/public/store/reducers/opensearch_reducer.ts @@ -7,7 +7,12 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { getRouteService } from '../../services'; import { Index, + IndexConfiguration, + IndexResponse, IngestPipelineConfig, + IngestPipelineResponse, + SearchPipelineConfig, + SearchPipelineResponse, OMIT_SYSTEM_INDEX_PATTERN, SimulateIngestPipelineDoc, } from '../../../common'; @@ -16,7 +21,13 @@ import { HttpFetchError } from '../../../../../src/core/public'; export const INITIAL_OPENSEARCH_STATE = { loading: false, errorMessage: '', + getIndexErrorMessage: '', + getSearchPipelineErrorMessage: '', + getIngestPipelineErrorMessage: '', indices: {} as { [key: string]: Index }, + indexDetails: {} as { [key: string]: IndexConfiguration }, + ingestPipelineDetails: {} as { [key: string]: IngestPipelineConfig }, + searchPipelineDetails: {} as { [key: string]: SearchPipelineConfig }, }; const OPENSEARCH_PREFIX = 'opensearch'; @@ -276,15 +287,15 @@ const opensearchSlice = createSlice({ }) .addCase(getIndex.pending, (state, action) => { state.loading = true; - state.errorMessage = ''; + state.getIndexErrorMessage = ''; }) .addCase(getIngestPipeline.pending, (state, action) => { state.loading = true; - state.errorMessage = ''; + state.getIngestPipelineErrorMessage = ''; }) .addCase(getSearchPipeline.pending, (state, action) => { state.loading = true; - state.errorMessage = ''; + state.getSearchPipelineErrorMessage = ''; }) .addCase(searchIndex.pending, (state, action) => { state.loading = true; @@ -308,16 +319,41 @@ const opensearchSlice = createSlice({ state.errorMessage = ''; }) .addCase(getIndex.fulfilled, (state, action) => { + const resourceDetailsMap = new Map(); + action.payload.forEach((index: IndexResponse) => { + resourceDetailsMap.set(index.indexName, index.indexDetails); + }); + state.indexDetails = Object.fromEntries(resourceDetailsMap.entries()); state.loading = false; - state.errorMessage = ''; + state.getIndexErrorMessage = ''; }) .addCase(getSearchPipeline.fulfilled, (state, action) => { + const resourceDetailsMap = new Map(); + action.payload.forEach((pipeline: SearchPipelineResponse) => { + resourceDetailsMap.set( + pipeline.pipelineId, + pipeline.searchPipelineDetails + ); + }); + state.searchPipelineDetails = Object.fromEntries( + resourceDetailsMap.entries() + ); state.loading = false; - state.errorMessage = ''; + state.getSearchPipelineErrorMessage = ''; }) .addCase(getIngestPipeline.fulfilled, (state, action) => { + const resourceDetailsMap = new Map(); + action.payload.forEach((pipeline: IngestPipelineResponse) => { + resourceDetailsMap.set( + pipeline.pipelineId, + pipeline.ingestPipelineDetails + ); + }); + state.ingestPipelineDetails = Object.fromEntries( + resourceDetailsMap.entries() + ); state.loading = false; - state.errorMessage = ''; + state.getIngestPipelineErrorMessage = ''; }) .addCase(searchIndex.fulfilled, (state, action) => { state.loading = false; @@ -336,15 +372,15 @@ const opensearchSlice = createSlice({ state.loading = false; }) .addCase(getIndex.rejected, (state, action) => { - state.errorMessage = action.payload as string; + state.getIndexErrorMessage = action.payload as string; state.loading = false; }) .addCase(getIngestPipeline.rejected, (state, action) => { - state.errorMessage = action.payload as string; + state.getIngestPipelineErrorMessage = action.payload as string; state.loading = false; }) .addCase(getSearchPipeline.rejected, (state, action) => { - state.errorMessage = action.payload as string; + state.getSearchPipelineErrorMessage = action.payload as string; state.loading = false; }) .addCase(searchIndex.rejected, (state, action) => { diff --git a/public/utils/utils.ts b/public/utils/utils.ts index 43cb21e1..34970ce6 100644 --- a/public/utils/utils.ts +++ b/public/utils/utils.ts @@ -35,7 +35,6 @@ import * as pluginManifest from '../../opensearch_dashboards.json'; import { DataSourceAttributes } from '../../../../src/plugins/data_source/common/data_sources'; import { SavedObject } from '../../../../src/core/public'; import semver from 'semver'; -import { getIndex, getIngestPipeline, getSearchPipeline } from '../store'; // Generate a random ID. Optionally add a prefix. Optionally // override the default # characters to generate. @@ -370,32 +369,56 @@ export const dataSourceFilterFn = ( ); }; -// Fetches Resource details data for a given resource item. -export const fetchResourceData = async ( - item: WorkflowResource, - dataSourceId: string, - dispatch: any +export const extractIdsByStepType = (resources: WorkflowResource[]) => { + const ids = resources.reduce( + ( + acc: { + indexIds: string[]; + ingestPipelineIds: string[]; + searchPipelineIds: string[]; + }, + item: WorkflowResource + ) => { + switch (item.stepType) { + case WORKFLOW_STEP_TYPE.CREATE_INDEX_STEP_TYPE: + acc.indexIds.push(item.id); + break; + case WORKFLOW_STEP_TYPE.CREATE_INGEST_PIPELINE_STEP_TYPE: + acc.ingestPipelineIds.push(item.id); + break; + case WORKFLOW_STEP_TYPE.CREATE_SEARCH_PIPELINE_STEP_TYPE: + acc.searchPipelineIds.push(item.id); + break; + } + return acc; + }, + { indexIds: [], ingestPipelineIds: [], searchPipelineIds: [] } + ); + + return { + indexIds: ids.indexIds.join(','), + ingestPipelineIds: ids.ingestPipelineIds.join(','), + searchPipelineIds: ids.searchPipelineIds.join(','), + }; +}; + +export const getErrorMessageForStepType = ( + stepType: WORKFLOW_STEP_TYPE, + getIndexErrorMessage: string, + getIngestPipelineErrorMessage: string, + getSearchPipelineErrorMessage: string ) => { - if (item.stepType === WORKFLOW_STEP_TYPE.CREATE_INGEST_PIPELINE_STEP_TYPE) { - return await dispatch( - getIngestPipeline({ pipelineId: item.id, dataSourceId }) - ).unwrap(); - } else if (item.stepType === WORKFLOW_STEP_TYPE.CREATE_INDEX_STEP_TYPE) { - return await dispatch( - getIndex({ - index: item.id, - dataSourceId, - }) - ).unwrap(); - } else if ( - item.stepType === WORKFLOW_STEP_TYPE.CREATE_SEARCH_PIPELINE_STEP_TYPE - ) { - return await dispatch( - getSearchPipeline({ - pipelineId: item.id, - dataSourceId, - }) - ).unwrap(); + switch (stepType) { + case WORKFLOW_STEP_TYPE.CREATE_INDEX_STEP_TYPE: + return getIndexErrorMessage; + + case WORKFLOW_STEP_TYPE.CREATE_INGEST_PIPELINE_STEP_TYPE: + return getIngestPipelineErrorMessage; + + case WORKFLOW_STEP_TYPE.CREATE_SEARCH_PIPELINE_STEP_TYPE: + return getSearchPipelineErrorMessage; + + default: + return ''; } - return null; }; diff --git a/server/routes/opensearch_routes_service.ts b/server/routes/opensearch_routes_service.ts index a0ced608..2ba07300 100644 --- a/server/routes/opensearch_routes_service.ts +++ b/server/routes/opensearch_routes_service.ts @@ -21,10 +21,13 @@ import { INGEST_PIPELINE_NODE_API_PATH, Index, IndexMappings, + IndexResponse, IngestPipelineConfig, + IngestPipelineResponse, SEARCH_INDEX_NODE_API_PATH, SEARCH_PIPELINE_NODE_API_PATH, SIMULATE_PIPELINE_NODE_API_PATH, + SearchPipelineResponse, SimulateIngestPipelineDoc, SimulateIngestPipelineResponse, } from '../../common'; @@ -398,7 +401,15 @@ export class OpenSearchRoutesService { const response = await callWithRequest('indices.get', { index, }); - return res.ok({ body: response }); + // re-formatting the results to match IndexResponse + const cleanedIndexDetails = Object.entries(response).map( + ([indexName, indexDetails]) => ({ + indexName, + indexDetails, + }) + ) as IndexResponse[]; + + return res.ok({ body: cleanedIndexDetails }); } catch (err: any) { return generateCustomError(res, err); } @@ -546,8 +557,15 @@ export class OpenSearchRoutesService { const response = await callWithRequest('ingest.getPipeline', { id: pipeline_id, }); - - return res.ok({ body: response }); + // re-formatting the results to match IngestPipelineResponse + const cleanedIngestPipelineDetails = Object.entries(response).map( + ([pipelineId, ingestPipelineDetails]) => ({ + pipelineId, + ingestPipelineDetails, + }) + ) as IngestPipelineResponse[]; + + return res.ok({ body: cleanedIngestPipelineDetails }); } catch (err: any) { return generateCustomError(res, err); } @@ -574,7 +592,15 @@ export class OpenSearchRoutesService { pipeline_id: pipeline_id, }); - return res.ok({ body: response }); + // re-formatting the results to match SearchPipelineResponse + const cleanedSearchPipelineDetails = Object.entries(response).map( + ([pipelineId, searchPipelineDetails]) => ({ + pipelineId, + searchPipelineDetails, + }) + ) as SearchPipelineResponse[]; + + return res.ok({ body: cleanedSearchPipelineDetails }); } catch (err: any) { return generateCustomError(res, err); }