Repository: opensearch-project/OpenSearch-Dashboards
Pull request #9770: Add resource API pattern
Author: login: kathyguo18
Labels: name: enhancement
description: New feature or request
name: discover
description: for discover reinvent
name: first-time-contributor
name: explore
Created: 2025-05-05T22:03:38Z | Merged: 2025-05-28T17:20:24Z

### Description
feat. Add resource API pattern in query_enhancements

for details see https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9770#issuecomment-2863807507


### Issues Resolved
[RFC] Add A Resource API in Query Enhancements [#9779](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9779)

## Screenshot

## Testing the changes
yarn test

## Changelog
- feat: Add resource API pattern in query_enhancements

### Check List

- [ ] All tests pass
  - [ ] `yarn test:jest`
  - [ ] `yarn test:jest_integration`
- [ ] New functionality includes testing.
- [ ] New functionality has been documented.
- [ ] Update [CHANGELOG.md](./../CHANGELOG.md)
- [ ] Commits are signed per the DCO using --signoff



Diff:
diff --git a/changelogs/fragments/9770.yml b/changelogs/fragments/9770.yml
new file mode 100644
index 000000000000..6aa246f61802
--- /dev/null
+++ b/changelogs/fragments/9770.yml
@@ -0,0 +1,2 @@
+feat:
+- Add resource API pattern in query_enhancements ([#9770](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9770))
\ No newline at end of file
diff --git a/src/plugins/query_enhancements/server/connections/managers/base_connection_manager.ts b/src/plugins/query_enhancements/server/connections/managers/base_connection_manager.ts
new file mode 100644
index 000000000000..90138cab5078
--- /dev/null
+++ b/src/plugins/query_enhancements/server/connections/managers/base_connection_manager.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+  HttpResponsePayload,
+  OpenSearchDashboardsRequest,
+  RequestHandlerContext,
+} from 'src/core/server';
+
+/**
+ * @experimental this class is experimental and might change in future releases.
+ */
+export abstract class BaseConnectionManager {
+  constructor() {}
+
+  abstract handlePostRequest(
+    context: RequestHandlerContext,
+    request: OpenSearchDashboardsRequest
+  ): Promise<HttpResponsePayload>;
+}
diff --git a/src/plugins/query_enhancements/server/connections/resource_manager_service.test.ts b/src/plugins/query_enhancements/server/connections/resource_manager_service.test.ts
new file mode 100644
index 000000000000..4e0c508490f9
--- /dev/null
+++ b/src/plugins/query_enhancements/server/connections/resource_manager_service.test.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { resourceManagerService } from './resource_manager_service';
+import { BaseConnectionManager } from './managers/base_connection_manager';
+
+describe('ResourceManagerService', () => {
+  let mockManager: jest.Mocked<BaseConnectionManager>;
+
+  beforeEach(() => {
+    mockManager = {} as jest.Mocked<BaseConnectionManager>;
+  });
+
+  const DATA_CONNECTION_TYPE = 'test-connection';
+  describe('register', () => {
+    it('should successfully register a new manager', () => {
+      resourceManagerService.register(DATA_CONNECTION_TYPE, mockManager);
+      expect(resourceManagerService.getManager(DATA_CONNECTION_TYPE)).toBe(mockManager);
+    });
+
+    it('should throw error when registering duplicate connection type', () => {
+      expect(() => {
+        resourceManagerService.register(DATA_CONNECTION_TYPE, mockManager);
+      }).toThrow(
+        `Manager for dataConnectionType ${DATA_CONNECTION_TYPE} is already registered. Unable to register another manager.`
+      );
+    });
+  });
+
+  describe('getManager', () => {
+    it('should return correct manager for registered connection type', () => {
+      const result = resourceManagerService.getManager(DATA_CONNECTION_TYPE);
+      expect(result).toBeDefined();
+    });
+
+    it('should return undefined for non-existent connection type', () => {
+      const nonExistentType = 'non-existent';
+      const result = resourceManagerService.getManager(nonExistentType);
+      expect(result).toBeUndefined();
+    });
+  });
+});
diff --git a/src/plugins/query_enhancements/server/connections/resource_manager_service.ts b/src/plugins/query_enhancements/server/connections/resource_manager_service.ts
new file mode 100644
index 000000000000..0fa1872de09f
--- /dev/null
+++ b/src/plugins/query_enhancements/server/connections/resource_manager_service.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { BaseConnectionManager } from './managers/base_connection_manager';
+
+/**
+ * @experimental this class is experimental and might change in future releases.
+ */
+class ResourceManagerService {
+  private readonly managers: Map<string, BaseConnectionManager>;
+  constructor() {
+    this.managers = new Map();
+  }
+
+  register(dataConnectionType: string, manager: BaseConnectionManager) {
+    if (this.managers.get(dataConnectionType) !== undefined) {
+      throw new Error(
+        `Manager for dataConnectionType ${dataConnectionType} is already registered. Unable to register another manager.`
+      );
+    }
+    this.managers.set(dataConnectionType, manager);
+  }
+
+  getManager(dataConnectionType: string): BaseConnectionManager | undefined {
+    return this.managers.get(dataConnectionType);
+  }
+}
+
+// export as singleton
+export const resourceManagerService = new ResourceManagerService();
diff --git a/src/plugins/query_enhancements/server/plugin.ts b/src/plugins/query_enhancements/server/plugin.ts
index dfaea828209d..8f5fbccd3116 100644
--- a/src/plugins/query_enhancements/server/plugin.ts
+++ b/src/plugins/query_enhancements/server/plugin.ts
@@ -29,6 +29,8 @@ import {
   QueryEnhancementsPluginStart,
 } from './types';
 import { OpenSearchEnhancements } from './utils';
+import { resourceManagerService } from './connections/resource_manager_service';
+import { BaseConnectionManager } from './connections/managers/base_connection_manager';
 
 export class QueryEnhancementsPlugin
   implements Plugin<QueryEnhancementsPluginSetup, QueryEnhancementsPluginStart> {
@@ -71,6 +73,7 @@ export class QueryEnhancementsPlugin
     data.search.registerSearchStrategy(SEARCH_STRATEGY.SQL_ASYNC, sqlAsyncSearchStrategy);
     data.search.registerSearchStrategy(SEARCH_STRATEGY.PPL_ASYNC, pplAsyncSearchStrategy);
 
+    // @ts-ignore https://github.com/opensearch-project/openSearch-Dashboards/issues/4274
     core.http.registerRouteHandlerContext('query_assist', () => ({
       logger: this.logger,
       configPromise: this.initializerContext.config
@@ -80,6 +83,7 @@ export class QueryEnhancementsPlugin
       dataSourceEnabled: !!dataSource,
     }));
 
+    // @ts-ignore https://github.com/opensearch-project/openSearch-Dashboards/issues/4274
     core.http.registerRouteHandlerContext('data_source_connection', () => ({
       logger: this.logger,
       configPromise: this.initializerContext.config
@@ -99,6 +103,8 @@ export class QueryEnhancementsPlugin
     this.logger.info('queryEnhancements: Setup complete');
     return {
       defineSearchStrategyRoute: defineSearchStrategyRouteProvider(this.logger, router),
+      registerResourceManager: (dataConnectionType: string, manager: BaseConnectionManager) =>
+        resourceManagerService.register(dataConnectionType, manager),
     };
   }
 
diff --git a/src/plugins/query_enhancements/server/routes/index.ts b/src/plugins/query_enhancements/server/routes/index.ts
index 2cda4a9f0cbf..49784d4d31a7 100644
--- a/src/plugins/query_enhancements/server/routes/index.ts
+++ b/src/plugins/query_enhancements/server/routes/index.ts
@@ -15,6 +15,7 @@ import { ISearchStrategy } from '../../../data/server';
 import { API } from '../../common';
 import { registerQueryAssistRoutes } from './query_assist';
 import { registerDataSourceConnectionsRoutes } from './data_source_connection';
+import { registerResourceRoutes } from './resources';
 
 /**
  * Coerce status code to 503 for 500 errors from dependency services. Only use
@@ -136,4 +137,5 @@ export function defineRoutes(
   });
   registerDataSourceConnectionsRoutes(router, client);
   registerQueryAssistRoutes(router);
+  registerResourceRoutes(router);
 }
diff --git a/src/plugins/query_enhancements/server/routes/resources/index.ts b/src/plugins/query_enhancements/server/routes/resources/index.ts
new file mode 100644
index 000000000000..33097d092eae
--- /dev/null
+++ b/src/plugins/query_enhancements/server/routes/resources/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { registerResourceRoutes } from './routes';
diff --git a/src/plugins/query_enhancements/server/routes/resources/routes.test.ts b/src/plugins/query_enhancements/server/routes/resources/routes.test.ts
new file mode 100644
index 000000000000..94d18a15dd2b
--- /dev/null
+++ b/src/plugins/query_enhancements/server/routes/resources/routes.test.ts
@@ -0,0 +1,117 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { registerResourceRoutes } from './routes';
+import { resourceManagerService } from '../../connections/resource_manager_service';
+import { BASE_API } from '../../../common';
+import supertest from 'supertest';
+import { BaseConnectionManager } from '../../connections/managers/base_connection_manager';
+import {
+  HttpResponsePayload,
+  OpenSearchDashboardsRequest,
+  RequestHandlerContext,
+} from '../../../../../core/server';
+import { setupServer } from '../../../../../core/server/test_utils';
+import { HttpService } from 'opensearch-dashboards/server/http';
+
+class TestManager extends BaseConnectionManager {
+  handlePostRequestSpy: (
+    context: RequestHandlerContext,
+    request: OpenSearchDashboardsRequest
+  ) => Promise<HttpResponsePayload>;
+  constructor(
+    handlePostRequestSpy: (
+      context: RequestHandlerContext,
+      request: OpenSearchDashboardsRequest
+    ) => Promise<HttpResponsePayload>
+  ) {
+    super();
+    this.handlePostRequestSpy = handlePostRequestSpy;
+  }
+
+  handlePostRequest(
+    context: RequestHandlerContext,
+    request: OpenSearchDashboardsRequest
+  ): Promise<HttpResponsePayload> {
+    return this.handlePostRequestSpy(context, request);
+  }
+}
+
+describe('Resource Routes', () => {
+  let testServer: HttpService;
+  const DATA_CONNECTION_TYPE = 'datatype';
+  const DATA_CONNECTION_ID = 'dataconnectionid';
+  const DATA_CONNECTION_RESOURCE_TYPE = 'resourceType';
+  const REQUEST_BODY = {
+    connection: {
+      id: DATA_CONNECTION_ID,
+      type: DATA_CONNECTION_TYPE,
+    },
+    resource: {
+      type: DATA_CONNECTION_RESOURCE_TYPE,
+    },
+  };
+
+  const testSetup = async () => {
+    const { server, httpSetup } = await setupServer();
+    const router = httpSetup.createRouter('');
+    registerResourceRoutes(router);
+    const dynamicConfigService = {
+      getClient: jest.fn(),
+      getAsyncLocalStore: jest.fn(),
+      createStoreFromRequest: jest.fn(),
+    };
+    await server.start({ dynamicConfigService });
+    testServer = server;
+    return httpSetup;
+  };
+
+  afterEach(async () => {
+    if (testServer) {
+      await testServer.stop();
+    }
+  });
+
+  it('should connect to manager based on data connection type', async () => {
+    const handlePostRequestSpy = jest.fn().mockResolvedValue({ message: 'succeed' });
+    const manager = new TestManager(handlePostRequestSpy);
+    jest
+      .spyOn(resourceManagerService, 'getManager')
+      .mockImplementation((type: string) => (type === DATA_CONNECTION_TYPE ? manager : undefined));
+
+    const httpSetup = await testSetup();
+    const result = await supertest(httpSetup.server.listener)
+      .post(`${BASE_API}/resources`)
+      .send({ ...REQUEST_BODY })
+      .expect(200);
+    expect(result.body.message).toEqual('succeed');
+  });
+
+  it('should return 404 when data connection type is not registered', async () => {
+    jest.spyOn(resourceManagerService, 'getManager').mockReturnValue(undefined);
+
+    const httpSetup = await testSetup();
+    const result = await supertest(httpSetup.server.listener)
+      .post(`${BASE_API}/resources`)
+      .send({ ...REQUEST_BODY })
+      .expect(404);
+    expect(result.body.message).toEqual('Not Found');
+  });
+
+  it('should return error when get resources failed', async () => {
+    const handlePostRequestSpy = jest.fn().mockRejectedValue({ message: 'failed' });
+    const manager = new TestManager(handlePostRequestSpy);
+    jest
+      .spyOn(resourceManagerService, 'getManager')
+      .mockImplementation((type: string) => (type === DATA_CONNECTION_TYPE ? manager : undefined));
+
+    const httpSetup = await testSetup();
+    const result = await supertest(httpSetup.server.listener)
+      .post(`${BASE_API}/resources`)
+      .send({ ...REQUEST_BODY })
+      .expect(503);
+    expect(result.body.message).toEqual('Unable to get resources');
+  });
+});
diff --git a/src/plugins/query_enhancements/server/routes/resources/routes.ts b/src/plugins/query_enhancements/server/routes/resources/routes.ts
new file mode 100644
index 000000000000..008858febd4d
--- /dev/null
+++ b/src/plugins/query_enhancements/server/routes/resources/routes.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { schema } from '@osd/config-schema';
+import { IRouter } from 'opensearch-dashboards/server';
+import { BASE_API } from '../../../common';
+import { resourceManagerService } from '../../connections/resource_manager_service';
+import { coerceStatusCode } from '..';
+
+/**
+ * Defines routes for resource APIs.
+ *
+ * @experimental This function is experimental and might change in future releases.
+ *
+ * @param router - The router instance.
+ */
+export function registerResourceRoutes(router: IRouter) {
+  router.post(
+    {
+      path: `${BASE_API}/resources`,
+      validate: {
+        body: schema.object({
+          connection: schema.object({
+            id: schema.string(),
+            type: schema.string(),
+          }),
+          resource: schema.object({
+            name: schema.maybe(schema.string()),
+            type: schema.string(),
+          }),
+          content: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+        }),
+      },
+    },
+    async (context, request, response) => {
+      const { type } = request.body.connection;
+      const manager = resourceManagerService.getManager(type);
+      if (!manager) {
+        return response.notFound();
+      }
+      try {
+        const resourcesResponse = await manager.handlePostRequest(context, request);
+        return response.ok({ body: resourcesResponse });
+      } catch (error) {
+        const errorObj = error as any;
+        return response.customError({
+          body: 'Unable to get resources',
+          ...errorObj,
+          statusCode: coerceStatusCode(errorObj.statusCode),
+        });
+      }
+    }
+  );
+}
diff --git a/src/plugins/query_enhancements/server/types.ts b/src/plugins/query_enhancements/server/types.ts
index b8694cccab8e..480acb4bb904 100644
--- a/src/plugins/query_enhancements/server/types.ts
+++ b/src/plugins/query_enhancements/server/types.ts
@@ -6,12 +6,14 @@
 import { ISearchStrategy, PluginSetup } from 'src/plugins/data/server';
 import { DataSourcePluginSetup } from 'src/plugins/data_source/server';
 import { IDataFrameResponse, IOpenSearchDashboardsSearchRequest } from '../../data/common';
+import { BaseConnectionManager } from './connections/managers/base_connection_manager';
 
 export interface QueryEnhancementsPluginSetup {
   defineSearchStrategyRoute: (
     id: string,
     searchStrategy: ISearchStrategy<IOpenSearchDashboardsSearchRequest, IDataFrameResponse>
   ) => void;
+  registerResourceManager: (dataConnectionType: string, manager: BaseConnectionManager) => void;
 }
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
 export interface QueryEnhancementsPluginStart {}
diff --git a/ts_error_baseline.json b/ts_error_baseline.json
index 4ba0408997e3..c18d803fc0d5 100644
--- a/ts_error_baseline.json
+++ b/ts_error_baseline.json
@@ -378,6 +378,20 @@
       "code": "TS7022",
       "message": "'config' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer."
     },
+    {
+      "file": "src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx",
+      "line": 231,
+      "column": 33,
+      "code": "TS7006",
+      "message": "Parameter 'key' implicitly has an 'any' type."
+    },
+    {
+      "file": "src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx",
+      "line": 232,
+      "column": 67,
+      "code": "TS2339",
+      "message": "Property 'getUserProvidedWithScope' does not exist on type 'IUiSettingsClient'."
+    },
     {
       "file": "src/plugins/advanced_settings/public/management_app/components/field/field.tsx",
       "line": 277,
@@ -3269,6 +3283,13 @@
       "code": "TS2459",
       "message": "Module '\"./data_source_selector\"' declares 'DataSourceOption' locally, but it is not exported."
     },
+    {
+      "file": "src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx",
+      "line": 584,
+      "column": 17,
+      "code": "TS2554",
+      "message": "Expected 2 arguments, but got 3."
+    },
     {
       "file": "src/plugins/data_source_management/public/components/data_source_view/data_source_view.test.tsx",
       "line": 63,
@@ -5257,6 +5278,13 @@
       "code": "TS2339",
       "message": "Property 'text' does not exist on type '{ value: SigV4ServiceName; inputDisplay: string; }'."
     },
+    {
+      "file": "src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx",
+      "line": 158,
+      "column": 25,
+      "code": "TS2345",
+      "message": "Argument of type '\"getUserProvidedWithScope\"' is not assignable to parameter of type 'keyof IUiSettingsClient'."
+    },
     {
       "file": "src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx",
       "line": 12,
@@ -5285,6 +5313,13 @@
       "code": "TS2345",
       "message": "Argument of type '{ id: string; title: any; endpoint: any; description: any; auth: any; datasourceversion: any; installedplugins: any; }' is not assignable to parameter of type 'DataSourceAttributes'."
     },
+    {
+      "file": "src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx",
+      "line": 129,
+      "column": 91,
+      "code": "TS2554",
+      "message": "Expected 2 arguments, but got 3."
+    },
     {
       "file": "src/plugins/data_source_management/public/components/frame_work_test/catlog_cache/cache_intercept.test.ts",
       "line": 53,
@@ -5586,6 +5621,27 @@
       "code": "TS2741",
       "message": "Property 'dataSourceVersion' is missing in type '{ title: string; endpoint: string; auth: { type: AuthType.NoAuth; credentials: undefined; }; name: AuthType.NoAuth; }' but required in type 'DataSourceAttributes'."
     },
+    {
+      "file": "src/plugins/data_source_management/public/components/utils.ts",
+      "line": 291,
+      "column": 65,
+      "code": "TS2554",
+      "message": "Expected 1 arguments, but got 2."
+    },
+    {
+      "file": "src/plugins/data_source_management/public/components/utils.ts",
+      "line": 296,
+      "column": 83,
+      "code": "TS2554",
+      "message": "Expected 2 arguments, but got 3."
+    },
+    {
+      "file": "src/plugins/data_source_management/public/components/utils.ts",
+      "line": 349,
+      "column": 37,
+      "code": "TS2339",
+      "message": "Property 'getUserProvidedWithScope' does not exist on type 'IUiSettingsClient'."
+    },
     {
       "file": "src/plugins/data_source_management/public/components/validation/datasource_form_validation.test.ts",
       "line": 62,
@@ -5614,6 +5670,13 @@
       "code": "TS2739",
       "message": "Type '{ history: ScopedHistory<unknown>; featureFlagStatus: boolean; useNewUX: any; }' is missing the following properties from type 'DataSourceHomePanelProps': location, match"
     },
+    {
+      "file": "src/plugins/data_source_management/public/mocks.ts",
+      "line": 497,
+      "column": 4,
+      "code": "TS7052",
+      "message": "Element implicitly has an 'any' type because type 'IUiSettingsClient' has no index signature. Did you mean to call 'uiSettings.get'?"
+    },
     {
       "file": "src/plugins/data_source_management/public/mocks.ts",
       "line": 525,
@@ -6902,6 +6965,20 @@
       "code": "TS6196",
       "message": "'DocParams' is declared but never used."
     },
+    {
+      "file": "src/plugins/explore/public/components/doc_viewer/doc_viewer.test.tsx",
+      "line": 8,
+      "column": 23,
+      "code": "TS2307",
+      "message": "Cannot find module '@testing-library/user-event' or its corresponding type declarations."
+    },
+    {
+      "file": "src/plugins/explore/public/plugin.ts",
+      "line": 63,
+      "column": 10,
+      "code": "TS2305",
+      "message": "Module '\"../../../core/public\"' has no exported member 'isNavGroupInFeatureConfigs'."
+    },
     {
       "file": "src/plugins/expressions/common/expression_types/expression_type.test.ts",
       "line": 58,
@@ -7862,18 +7939,32 @@
       "message": "All imports in import declaration are unused."
     },
     {
-      "file": "src/plugins/query_enhancements/server/plugin.ts",
-      "line": 74,
-      "column": 43,
-      "code": "TS2345",
-      "message": "Argument of type '\"query_assist\"' is not assignable to parameter of type 'keyof RequestHandlerContext'."
+      "file": "src/plugins/query_enhancements/server/routes/data_source_connection/routes.ts",
+      "line": 28,
+      "column": 28,
+      "code": "TS2554",
+      "message": "Expected 2-3 arguments, but got 1."
     },
     {
-      "file": "src/plugins/query_enhancements/server/plugin.ts",
-      "line": 83,
-      "column": 43,
-      "code": "TS2345",
-      "message": "Argument of type '\"data_source_connection\"' is not assignable to parameter of type 'keyof RequestHandlerContext'."
+      "file": "src/plugins/query_enhancements/server/routes/data_source_connection/routes.ts",
+      "line": 29,
+      "column": 30,
+      "code": "TS2322",
+      "message": "Type 'unknown' is not assignable to type 'HttpResponsePayload'."
+    },
+    {
+      "file": "src/plugins/query_enhancements/server/routes/data_source_connection/routes.ts",
+      "line": 63,
+      "column": 30,
+      "code": "TS2322",
+      "message": "Type 'unknown' is not assignable to type 'HttpResponsePayload'."
+    },
+    {
+      "file": "src/plugins/query_enhancements/server/routes/data_source_connection/routes.ts",
+      "line": 94,
+      "column": 30,
+      "code": "TS2322",
+      "message": "Type 'unknown' is not assignable to type 'HttpResponsePayload'."
     },
     {
       "file": "src/plugins/query_enhancements/server/routes/data_source_connection/routes.ts",
@@ -10192,6 +10283,13 @@
       "code": "TS2322",
       "message": "Type 'BehaviorSubject<{ features: { id: string; }[]; id: string; title: string; description: string; icon: string; }[]>' is not assignable to type 'BehaviorSubject<WorkspaceUseCase[]>'."
     },
+    {
+      "file": "src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx",
+      "line": 37,
+      "column": 10,
+      "code": "TS2305",
+      "message": "Module '\"../../../../../core/public\"' has no exported member 'getUseCaseFeatureConfig'."
+    },
     {
       "file": "src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx",
       "line": 225,
@@ -10360,6 +10458,13 @@
       "code": "TS2322",
       "message": "Type '{ id: string; name: string; features: string[]; }' is not assignable to type 'WorkspaceFormSubmitData'."
     },
+    {
+      "file": "src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts",
+      "line": 26,
+      "column": 10,
+      "code": "TS2305",
+      "message": "Module '\"../../../../../core/public\"' has no exported member 'getUseCaseFeatureConfig'."
+    },
     {
       "file": "src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.test.tsx",
       "line": 20,
@@ -10563,6 +10668,13 @@
       "code": "TS6133",
       "message": "'screen' is declared but its value is never read."
     },
+    {
+      "file": "src/plugins/workspace/public/plugin.ts",
+      "line": 75,
+      "column": 10,
+      "code": "TS2305",
+      "message": "Module '\"opensearch-dashboards/public\"' has no exported member 'isNavGroupInFeatureConfigs'."
+    },
     {
       "file": "src/plugins/workspace/public/register_default_collaborator_types.test.tsx",
       "line": 20,
@@ -10584,6 +10696,20 @@
       "code": "TS2322",
       "message": "Type 'BehaviorSubject<{ id: string; title: string; }[]>' is not assignable to type 'Observable<PublicAppInfo[]>'."
     },
+    {
+      "file": "src/plugins/workspace/public/utils.test.ts",
+      "line": 22,
+      "column": 10,
+      "code": "TS2305",
+      "message": "Module '\"../../../core/public\"' has no exported member 'WORKSPACE_USE_CASE_PREFIX'."
+    },
+    {
+      "file": "src/plugins/workspace/public/utils.ts",
+      "line": 24,
+      "column": 3,
+      "code": "TS2305",
+      "message": "Module '\"../../../core/public\"' has no exported member 'WORKSPACE_USE_CASE_PREFIX'."
+    },
     {
       "file": "src/plugins/workspace/public/utils.ts",
       "line": 313,
@@ -10668,6 +10794,13 @@
       "code": "TS2345",
       "message": "Argument of type '{ router: IRouter; client: IWorkspaceClientImpl; logger: Logger; maxImportExportSize: number; isPermissionControlEnabled: false; }' is not assignable to parameter of type '{ client: IWorkspaceClientImpl; logger: Logger; router: IRouter; maxImportExportSize: number; permissionControlClient?: SavedObjectsPermissionControlContract | undefined; isPermissionControlEnabled: boolean; isDataSourceEnabled: boolean; }'."
     },
+    {
+      "file": "src/plugins/workspace/server/routes/index.ts",
+      "line": 24,
+      "column": 10,
+      "code": "TS2305",
+      "message": "Module '\"src/core/server\"' has no exported member 'getUseCaseFeatureConfig'."
+    },
     {
       "file": "src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts",
       "line": 471,
@@ -10695,8 +10828,22 @@
       "column": 52,
       "code": "TS2322",
       "message": "Type '({ type: string; id: string; attributes: { description: string; }; references: string[]; workspaces: string[]; error?: undefined; } | { type: string; id: string; attributes: { description?: undefined; }; references: never[]; error: { ...; }; workspaces?: undefined; } | { ...; } | { ...; })[]' is not assignable to type 'SavedObject<unknown>[]'."
+    },
+    {
+      "file": "src/plugins/workspace/server/saved_objects/workspace_ui_settings_client_wrapper.test.ts",
+      "line": 11,
+      "column": 3,
+      "code": "TS2305",
+      "message": "Module '\"src/core/server\"' has no exported member 'CURRENT_WORKSPACE_PLACEHOLDER'."
+    },
+    {
+      "file": "src/plugins/workspace/server/saved_objects/workspace_ui_settings_client_wrapper.ts",
+      "line": 19,
+      "column": 3,
+      "code": "TS2305",
+      "message": "Module '\"src/core/server\"' has no exported member 'CURRENT_WORKSPACE_PLACEHOLDER'."
     }
   ],
-  "updatedAt": "2025-05-22T17:12:17.470Z",
+  "updatedAt": "2025-05-23T20:52:34.624Z",
   "tsconfig": "/Users/jungkm/workplace/OpenSearch-Dashboards/tsconfig.check.json"
 }
\ No newline at end of file


Reviews:
user: login: joshuali925
body: you had an older PR https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9731. if one is outdated, could you close it?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T23:42:32Z
user: login: kathyguo18
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T03:26:32Z
user: login: joshuali925
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-07T03:31:17Z
user: login: angle943
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T16:34:31Z
user: login: angle943
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T16:36:49Z
user: login: angle943
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T16:39:59Z
user: login: angle943
body: Looks good, i think just the addition of `@experimental` flag is needed as @ashwin-pc suggested and i'll approve
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T16:41:24Z
user: login: joshuali925
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T18:10:00Z
user: login: kathyguo18
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:12:47Z
user: login: joshuali925
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T20:11:26Z
user: login: kathyguo18
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T20:25:41Z
user: login: joshuali925
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-09T20:36:10Z
user: login: angle943
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-05-09T21:21:17Z
user: login: kavilla
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T09:24:56Z
user: login: kavilla
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T09:31:28Z
user: login: kavilla
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-14T09:32:17Z
user: login: joshuali925
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-23T20:02:14Z
user: login: kavilla
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-27T17:32:36Z
user: login: joshuali925
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-28T17:19:58Z

Review comments:
diff_hunk: @@ -0,0 +1,49 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { schema } from '@osd/config-schema';
+import { IRouter } from 'opensearch-dashboards/server';
+import { BASE_API } from '../../../common';
+import { resourceManagerService } from '../../connections/resource_manager_service';
+import { coerceStatusCode } from '..';
+
+export function registerResourceRoutes(router: IRouter) {
+  router.post(
+    {
+      path: `${BASE_API}/resources`,
+      validate: {
+        body: schema.object({
+          connection: schema.object({
+            id: schema.string(),
+            type: schema.string(),
+          }),
+          resource: schema.object({
+            name: schema.maybe(schema.string()),
+            type: schema.string(),
+          }),
+          content: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+        }),
+      },
+    },
+    async (context, request, response) => {
+      const { type } = request.body.connection;
+      const manager = resourceManagerService.getManager(type);
+      if (!manager) {
+        return response.notFound();
+      }
+      try {
+        const resourcesResponse = await manager.handleRequest(context, request);
path: src/plugins/query_enhancements/server/routes/resources/routes.ts
user: login: joshuali925
body: i would prefer this to be more explicit, rather than handling all CRUD operations in a single `handleRequest` function. so similar question as https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9731#discussion_r2072047823: consider for example, when we add prometheus delete alerts API, should it be routing to `manager.handleRequest`? Prometheus APIs are restful, where API paths are resources, how does handleRequest know how to distinguish the operation?
created_at: 2025-05-06T23:40:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,49 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { schema } from '@osd/config-schema';
+import { IRouter } from 'opensearch-dashboards/server';
+import { BASE_API } from '../../../common';
+import { resourceManagerService } from '../../connections/resource_manager_service';
+import { coerceStatusCode } from '..';
+
+export function registerResourceRoutes(router: IRouter) {
+  router.post(
+    {
+      path: `${BASE_API}/resources`,
+      validate: {
+        body: schema.object({
+          connection: schema.object({
+            id: schema.string(),
+            type: schema.string(),
+          }),
+          resource: schema.object({
+            name: schema.maybe(schema.string()),
+            type: schema.string(),
+          }),
+          content: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+        }),
+      },
+    },
+    async (context, request, response) => {
+      const { type } = request.body.connection;
+      const manager = resourceManagerService.getManager(type);
+      if (!manager) {
+        return response.notFound();
+      }
+      try {
+        const resourcesResponse = await manager.handleRequest(context, request);
path: src/plugins/query_enhancements/server/routes/resources/routes.ts
user: login: kathyguo18
body: Thanks for the comment! Updated to handlePostRequest.
created_at: 2025-05-07T03:26:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { BaseConnectionManager } from './managers/base_connection_manager';
+
+class ResourceManagerService {
+  private readonly managers: { [key: string]: BaseConnectionManager };
+  constructor() {
+    this.managers = {};
+  }
+
+  register(dataConnectionType: string, manager: BaseConnectionManager) {
+    if (this.managers[dataConnectionType] !== undefined) {
+      throw new Error(
+        `Manager for dataConnectionType ${dataConnectionType} is already registered. Unable to register another manager.`
+      );
+    }
+    this.managers[dataConnectionType] = manager;
+  }
+
+  getManager(dataConnectionType: string): BaseConnectionManager | undefined {
+    return this.managers[dataConnectionType];
+  }
+}
path: src/plugins/query_enhancements/server/connections/resource_manager_service.ts
user: login: angle943
body: is there a use case where we want to delete a manager? If so should we expose that as a method?
created_at: 2025-05-09T16:34:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,29 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { BaseConnectionManager } from './managers/base_connection_manager';
+
+class ResourceManagerService {
+  private readonly managers: { [key: string]: BaseConnectionManager };
+  constructor() {
+    this.managers = {};
path: src/plugins/query_enhancements/server/connections/resource_manager_service.ts
user: login: angle943
body: super nitpick: i think a Map would be better suited here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
created_at: 2025-05-09T16:36:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { schema } from '@osd/config-schema';
+import { IRouter } from 'opensearch-dashboards/server';
+import { BASE_API } from '../../../common';
+import { resourceManagerService } from '../../connections/resource_manager_service';
+import { coerceStatusCode } from '..';
+
+export function registerResourceRoutes(router: IRouter) {
+  router.post(
+    {
+      path: `${BASE_API}/resources`,
path: src/plugins/query_enhancements/server/routes/resources/routes.ts
user: login: angle943
body: nitpick: do we always use hardcoded string for the route? IE the `resources` part.
created_at: 2025-05-09T16:39:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,29 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { BaseConnectionManager } from './managers/base_connection_manager';
+
+class ResourceManagerService {
+  private readonly managers: { [key: string]: BaseConnectionManager };
+  constructor() {
+    this.managers = {};
+  }
+
+  register(dataConnectionType: string, manager: BaseConnectionManager) {
+    if (this.managers[dataConnectionType] !== undefined) {
+      throw new Error(
+        `Manager for dataConnectionType ${dataConnectionType} is already registered. Unable to register another manager.`
+      );
+    }
+    this.managers[dataConnectionType] = manager;
+  }
+
+  getManager(dataConnectionType: string): BaseConnectionManager | undefined {
+    return this.managers[dataConnectionType];
+  }
+}
path: src/plugins/query_enhancements/server/connections/resource_manager_service.ts
user: login: joshuali925
body: i don't think that would be needed, registering manager happens one time during plugin setup, i don't see a reasonable trigger on when would a plugin want to delete a manager
created_at: 2025-05-09T18:10:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,49 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { schema } from '@osd/config-schema';
+import { IRouter } from 'opensearch-dashboards/server';
+import { BASE_API } from '../../../common';
+import { resourceManagerService } from '../../connections/resource_manager_service';
+import { coerceStatusCode } from '..';
+
+export function registerResourceRoutes(router: IRouter) {
+  router.post(
+    {
+      path: `${BASE_API}/resources`,
path: src/plugins/query_enhancements/server/routes/resources/routes.ts
user: login: kathyguo18
body: Yes. This new API is make to be a generic API to retrieve resources. So it's always the hardcoded string "resources". Besides POST, GET/DELETE/PUT may be added in the future.
created_at: 2025-05-09T18:12:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { schema } from '@osd/config-schema';
+import { IRouter } from 'opensearch-dashboards/server';
+import { BASE_API } from '../../../common';
+import { resourceManagerService } from '../../connections/resource_manager_service';
+import { coerceStatusCode } from '..';
+
+export function registerResourceRoutes(router: IRouter) {
+  router.post(
+    {
+      path: `${BASE_API}/resources`,
path: src/plugins/query_enhancements/server/routes/resources/routes.ts
user: login: joshuali925
body: I personally prefer to define APIs like (not relevant to this PR, just an example
```ts
export const API = {
  RESOURCES: '/api/query_enhancements/v1/resources',
  RESOURCE: '/api/query_enhancements/v1/resource',
} as const;
```

this makes it easy to grep when debugging, and I remember joined string literals is no longer a string literal due to our old typescript version, it causes lsp hover to show `string` when hovering on `API.RESOURCES` rather than the actual string literal

But I don't think there's a documented standard, i see all different things in OSD
created_at: 2025-05-09T20:11:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,29 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { BaseConnectionManager } from './managers/base_connection_manager';
+
+class ResourceManagerService {
+  private readonly managers: { [key: string]: BaseConnectionManager };
+  constructor() {
+    this.managers = {};
path: src/plugins/query_enhancements/server/connections/resource_manager_service.ts
user: login: kathyguo18
body: Thanks for the comment! Updated to use Map.
created_at: 2025-05-09T20:25:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+  HttpResponsePayload,
+  OpenSearchDashboardsRequest,
+  RequestHandlerContext,
+} from 'src/core/server';
+
+/**
+ * @experimental this class is experimental and might change in future releases.
path: src/plugins/query_enhancements/server/connections/managers/base_connection_manager.ts
user: login: kavilla
body: thank you for this
created_at: 2025-05-14T09:24:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,32 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { BaseConnectionManager } from './managers/base_connection_manager';
+
+/**
+ * @experimental this class is experimental and might change in future releases.
+ */
+class ResourceManagerService {
+  private readonly managers: Map<string, BaseConnectionManager>;
+  constructor() {
+    this.managers = new Map();
+  }
+
+  register(dataConnectionType: string, manager: BaseConnectionManager) {
+    if (this.managers.get(dataConnectionType) !== undefined) {
+      throw new Error(
+        `Manager for dataConnectionType ${dataConnectionType} is already registered. Unable to register another manager.`
+      );
+    }
+    this.managers.set(dataConnectionType, manager);
+  }
+
+  getManager(dataConnectionType: string): BaseConnectionManager | undefined {
+    return this.managers.get(dataConnectionType);
+  }
+}
+
+// export as singleton
+export const resourceManagerService = new ResourceManagerService();
path: src/plugins/query_enhancements/server/connections/resource_manager_service.ts
user: login: kavilla
body: nit: the way core does this is via getters and setters. https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/data/public/services.ts#L37 

just a heads up placing this in query enhancements could be a little risky. query enhancements is optionally enabled. technically we can disable this plugin and now the resource manager service will no longer exist. 

typically we try to rely on key plugins with registration points that if disabled OSD does not function at all.

perhaps that could be a follow up. more so in protection of consumer plugins not relying on an optional plugin
created_at: 2025-05-14T09:31:28Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-05-06T03:55:51Z
body: ## [Codecov](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) Report
Attention: Patch coverage is `94.11765%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 61.71%. Comparing base [(`819bd9a`)](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/commit/819bd9a5cc03a6f72ecbe4c254792e70354fd4cf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) to head [(`693a590`)](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/commit/693a59056b8ac7b1ee0a86d2b5dd24711b6e474c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project).
> Report is 4 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | Patch % | Lines |
|---|---|---|
| [.../plugins/query\_enhancements/server/routes/index.ts](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770?src=pr&el=tree&filepath=src%2Fplugins%2Fquery_enhancements%2Fserver%2Froutes%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project#diff-c3JjL3BsdWdpbnMvcXVlcnlfZW5oYW5jZW1lbnRzL3NlcnZlci9yb3V0ZXMvaW5kZXgudHM=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #9770   +/-   ##
=======================================
  Coverage   61.70%   61.71%           
=======================================
  Files        3988     3990    +2     
  Lines       95245    95262   +17     
  Branches    15383    15385    +2     
=======================================
+ Hits        58774    58792   +18     
  Misses      32595    32595           
+ Partials     3876     3875    -1     
```

| [Flag](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | Coverage Δ | |
|---|---|---|
| [Linux_1](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | `28.84% <ø> (ø)` | |
| [Linux_2](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | `56.40% <ø> (ø)` | |
| [Linux_3](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | `39.55% <ø> (+<0.01%)` | :arrow_up: |
| [Linux_4](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | `30.10% <94.11%> (+0.43%)` | :arrow_up: |
| [Windows_1](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | `28.86% <ø> (ø)` | |
| [Windows_2](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | `56.36% <ø> (ø)` | |
| [Windows_3](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | `39.55% <ø> (ø)` | |
| [Windows_4](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | `30.10% <94.11%> (+0.43%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards/pull/9770?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: joshuali925
created_at: 2025-05-08T17:42:03Z
body: adding more context:

This PR allows plugins depending on query enhancements to register `BaseConnectionManager` implementations through a `ResourceManagerService`. 
```tsx
export abstract class BaseConnectionManager {
  constructor() {}

  abstract handlePostRequest(
    context: RequestHandlerContext,
    request: OpenSearchDashboardsRequest
  ): Promise<HttpResponsePayload>;
}
```

Additionally, it exposes an API 
```json5
POST /api/enhancements/resources
{
  "connection": {
    "id": "string",
    "type": "string"
  },
  "resource": {
    "name": "string", // optional
    "type": "string"
  },
  "content": {} // arbitrary JSON
}
```

This enables plugins to define their own logic to retrieve resources from external datasources, and expose them to other plugins in OSD through a single API.

Currently only `POST` is added, but we should support GET, PUT, DELETE methods later.

The implementation is extracted and modified from [prometheus branch](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/feature/prometheus). Currently there are no `BaseConnectionManager` implementations in the `main` branch.
author_association: MEMBER
user: login: ashwin-pc
created_at: 2025-05-09T02:17:33Z
body: Thanks for the RFC @kathyguo18. Can you mark this feature as experimental? we might be moving the query API to core and in which case we might not want to keep this here as well. you can see other examples of this in the code.
author_association: MEMBER
user: login: kathyguo18
created_at: 2025-05-09T20:30:25Z
body: > Looks good, i think just the addition of `@experimental` flag is needed as @ashwin-pc suggested and i'll approve

Thanks for reviewing! Added `@experimental` flag in registerResourceRoutes function, BaseConnectionManager class, and ResourceManagerService class. 
author_association: CONTRIBUTOR
user: login: angle943
created_at: 2025-05-21T02:09:12Z
body: @kavilla do you know why this is failing? https://github.com/opensearch-project/OpenSearch-Dashboards/actions/runs/15150059713/job/42594604840?pr=9770
author_association: COLLABORATOR
user: login: ZilongX
created_at: 2025-06-04T19:55:22Z
body: is there a target release tag for this change ?
author_association: COLLABORATOR