diff --git a/common/constants.ts b/common/constants.ts index 2eabeefc..2c6fcfed 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -5,4 +5,6 @@ export const PLUGIN_ID = 'aiFlowDashboards'; export const BASE_NODE_API_PATH = '/api/ai_flow'; -export const SEARCH_PATH = `${BASE_NODE_API_PATH}/search`; +export const BASE_INDICES_NODE_API_PATH = `${BASE_NODE_API_PATH}/indices`; +export const SEARCH_INDICES_PATH = `${BASE_INDICES_NODE_API_PATH}/search`; +export const FETCH_INDICES_PATH = `${BASE_INDICES_NODE_API_PATH}/fetch`; diff --git a/common/index.ts b/common/index.ts index 2e209c79..f318d4bd 100644 --- a/common/index.ts +++ b/common/index.ts @@ -4,3 +4,4 @@ */ export * from './constants'; +export * from './interfaces'; diff --git a/common/interfaces.ts b/common/interfaces.ts new file mode 100644 index 00000000..cff651e8 --- /dev/null +++ b/common/interfaces.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Interfaces here are primarily used for standardizing the data across + * server & client side + */ + +export interface IIndex { + name: string; + health: 'green' | 'yellow' | 'red'; +} diff --git a/public/route_service.ts b/public/route_service.ts index 71296e62..54b2a76e 100644 --- a/public/route_service.ts +++ b/public/route_service.ts @@ -4,10 +4,11 @@ */ import { CoreStart, HttpFetchError } from '../../../src/core/public'; -import { SEARCH_PATH } from '../common'; +import { FETCH_INDICES_PATH, SEARCH_INDICES_PATH } from '../common'; export interface RouteService { searchIndex: (indexName: string, body: {}) => Promise; + fetchIndices: (pattern: string) => Promise; } export function configureRoutes(core: CoreStart): RouteService { @@ -15,7 +16,7 @@ export function configureRoutes(core: CoreStart): RouteService { searchIndex: async (indexName: string, body: {}) => { try { const response = await core.http.post<{ respString: string }>( - `${SEARCH_PATH}/${indexName}`, + `${SEARCH_INDICES_PATH}/${indexName}`, { body: JSON.stringify(body), } @@ -25,5 +26,15 @@ export function configureRoutes(core: CoreStart): RouteService { return e as HttpFetchError; } }, + fetchIndices: async (pattern: string) => { + try { + const response = await core.http.post<{ respString: string }>( + `${FETCH_INDICES_PATH}/${pattern}` + ); + return response; + } catch (e: any) { + return e as HttpFetchError; + } + }, }; } diff --git a/public/store/reducers/index.ts b/public/store/reducers/index.ts index 2f5cadc0..99da2fd8 100644 --- a/public/store/reducers/index.ts +++ b/public/store/reducers/index.ts @@ -4,3 +4,4 @@ */ export * from './workspace_reducer'; +export * from './opensearch_reducer'; diff --git a/public/store/reducers/opensearch_reducer.ts b/public/store/reducers/opensearch_reducer.ts new file mode 100644 index 00000000..8bc8a53f --- /dev/null +++ b/public/store/reducers/opensearch_reducer.ts @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { getRouteService } from '../../services'; +import { IIndex } from '../../../common'; + +const initialState = { + loading: false, + errorMessage: '', + indices: {} as { [key: string]: IIndex }, +}; + +const OPENSEARCH_PREFIX = 'opensearch'; +const FETCH_INDICES_ACTION = `${OPENSEARCH_PREFIX}/fetchIndices`; + +export const fetchIndices = createAsyncThunk( + FETCH_INDICES_ACTION, + async (pattern?: string) => { + // defaulting to fetch everything except system indices (starting with '.') + const patternString = pattern || '*,-.*'; + const response = getRouteService().fetchIndices(patternString); + return response; + } +); + +const opensearchSlice = createSlice({ + name: OPENSEARCH_PREFIX, + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchIndices.pending, (state, action) => { + state.loading = true; + }) + .addCase(fetchIndices.fulfilled, (state, action) => { + const indicesMap = new Map(); + action.payload.forEach((index: IIndex) => { + indicesMap.set(index.name, index); + }); + state.indices = Object.fromEntries(indicesMap.entries()); + state.loading = false; + }) + .addCase(fetchIndices.rejected, (state, action) => { + state.errorMessage = action.payload as string; + state.loading = false; + }); + }, +}); + +export const opensearchReducer = opensearchSlice.reducer; diff --git a/public/store/store.ts b/public/store/store.ts index 9359d352..1d73d5cf 100644 --- a/public/store/store.ts +++ b/public/store/store.ts @@ -5,10 +5,11 @@ import { configureStore } from '@reduxjs/toolkit'; import { combineReducers } from 'redux'; -import { workspaceReducer } from './reducers'; +import { workspaceReducer, opensearchReducer } from './reducers'; const rootReducer = combineReducers({ workspace: workspaceReducer, + opensearch: opensearchReducer, }); export const store = configureStore({ reducer: rootReducer, diff --git a/server/routes/helpers.ts b/server/routes/helpers.ts new file mode 100644 index 00000000..4ffa1ea1 --- /dev/null +++ b/server/routes/helpers.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// OSD does not provide an interface for this response, but this is following the suggested +// implementations. To prevent typescript complaining, leaving as loosely-typed 'any' +export function generateCustomError(res: any, err: any) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); +} diff --git a/server/routes/opensearch_routes.ts b/server/routes/opensearch_routes.ts index 2b7f2563..6e6b81f3 100644 --- a/server/routes/opensearch_routes.ts +++ b/server/routes/opensearch_routes.ts @@ -9,12 +9,13 @@ import { IRouter, IOpenSearchDashboardsResponse, } from '../../../../src/core/server'; -import { SEARCH_PATH } from '../../common'; +import { SEARCH_INDICES_PATH, FETCH_INDICES_PATH, IIndex } from '../../common'; +import { generateCustomError } from './helpers'; export function registerOpenSearchRoutes(router: IRouter): void { router.post( { - path: `${SEARCH_PATH}/{index_name}`, + path: `${SEARCH_INDICES_PATH}/{index_name}`, validate: { params: schema.object({ index_name: schema.string(), @@ -37,15 +38,38 @@ export function registerOpenSearchRoutes(router: IRouter): void { const response = await client.search(params); return res.ok({ body: response }); } catch (err: any) { - return res.customError({ - statusCode: err.statusCode || 500, - body: { - message: err.message, - attributes: { - error: err.body?.error || err.message, - }, - }, + return generateCustomError(res, err); + } + } + ); + router.post( + { + path: `${FETCH_INDICES_PATH}/{pattern}`, + validate: { + params: schema.object({ + pattern: schema.string(), + }), + }, + }, + async (context, req, res): Promise> => { + const client = context.core.opensearch.client.asCurrentUser; + const { pattern } = req.params; + try { + const response = await client.cat.indices({ + index: pattern, + format: 'json', + h: 'health,index', }); + + // re-formatting the index results to match IIndex + const cleanedIndices = response.body.map((index) => ({ + name: index.index, + health: index.health, + })) as IIndex[]; + + return res.ok({ body: cleanedIndices }); + } catch (err: any) { + return generateCustomError(res, err); } } );