diff --git a/changelogs/fragments/8838.yml b/changelogs/fragments/8838.yml new file mode 100644 index 000000000000..a714dca4f0ae --- /dev/null +++ b/changelogs/fragments/8838.yml @@ -0,0 +1,2 @@ +fix: +- [MDS] Fix showing DQS sources list in workspaces ([#8838](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8838)) \ No newline at end of file diff --git a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/__snapshots__/manage_direct_query_data_connections_table.test.tsx.snap b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/__snapshots__/manage_direct_query_data_connections_table.test.tsx.snap index f9633228bfc1..e34533542ab6 100644 --- a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/__snapshots__/manage_direct_query_data_connections_table.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/__snapshots__/manage_direct_query_data_connections_table.test.tsx.snap @@ -1,5 +1,4005 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`ManageDirectQueryDataConnectionsTable fetch security lake and cloudwatch direct query connections should render empty table 1`] = ` + + + +
+ +
+ +
+ + AWS CloudWatch + , + }, + Object { + "name": "AWS Security Lake", + "value": "AWS Security Lake", + "view": + AWS Security Lake + , + }, + ], + "type": "field_value_selection", + }, + ], + "toolsLeft": Array [], + } + } + selection={ + Object { + "onSelectionChange": [Function], + "selectable": [Function], + } + } + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "title", + }, + } + } + tableLayout="auto" + > +
+ + AWS CloudWatch + , + }, + Object { + "name": "AWS Security Lake", + "value": "AWS Security Lake", + "view": + AWS Security Lake + , + }, + ], + "type": "field_value_selection", + }, + ] + } + onChange={[Function]} + toolsLeft={Array []} + > + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ +
+ + AWS CloudWatch + , + }, + Object { + "name": "AWS Security Lake", + "value": "AWS Security Lake", + "view": + AWS Security Lake + , + }, + ], + "type": "field_value_selection", + }, + ] + } + onChange={[Function]} + query={ + Query { + "ast": _AST { + "_clauses": Array [], + "_indexedClauses": Object { + "field": Object {}, + "group": Array [], + "is": Object {}, + "term": Array [], + }, + }, + "syntax": Object { + "parse": [Function], + "print": [Function], + "printClause": [Function], + }, + "text": "", + } + } + > + +
+ + AWS CloudWatch + , + }, + Object { + "name": "AWS Security Lake", + "value": "AWS Security Lake", + "view": + AWS Security Lake + , + }, + ], + "type": "field_value_selection", + } + } + index={0} + onChange={[Function]} + query={ + Query { + "ast": _AST { + "_clauses": Array [], + "_indexedClauses": Object { + "field": Object {}, + "group": Array [], + "is": Object {}, + "term": Array [], + }, + }, + "syntax": Object { + "parse": [Function], + "print": [Function], + "printClause": [Function], + }, + "text": "", + } + } + > + + Type + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="field_value_selection_0" + isOpen={false} + ownFocus={true} + panelClassName="euiFilterGroup__popoverPanel" + panelPaddingSize="none" + > +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + Type + + + + + + + + + + + + Description + + + + + + + + + + + + Related connections + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+
+
+ Data source +
+
+ + + Connection 1 + +
+
+
+ Type +
+
+ + AWS CloudWatch + +
+
+
+ +
+
+
+ Related connections +
+
+
+
+ + +
+ +
+
+ + +
+
+
+
+
+ Data source +
+
+ + + Connection 2 + +
+
+
+ Type +
+
+ + AWS Security Lake + +
+
+
+ +
+
+
+ Related connections +
+
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+ +
+ +
+ + + +`; + +exports[`ManageDirectQueryDataConnectionsTable fetch security lake and cloudwatch direct query connections should render normally 1`] = ` + + + +
+ +
+ +
+ + AWS CloudWatch + , + }, + Object { + "name": "AWS Security Lake", + "value": "AWS Security Lake", + "view": + AWS Security Lake + , + }, + ], + "type": "field_value_selection", + }, + ], + "toolsLeft": Array [], + } + } + selection={ + Object { + "onSelectionChange": [Function], + "selectable": [Function], + } + } + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "title", + }, + } + } + tableLayout="auto" + > +
+ + AWS CloudWatch + , + }, + Object { + "name": "AWS Security Lake", + "value": "AWS Security Lake", + "view": + AWS Security Lake + , + }, + ], + "type": "field_value_selection", + }, + ] + } + onChange={[Function]} + toolsLeft={Array []} + > + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ +
+ + AWS CloudWatch + , + }, + Object { + "name": "AWS Security Lake", + "value": "AWS Security Lake", + "view": + AWS Security Lake + , + }, + ], + "type": "field_value_selection", + }, + ] + } + onChange={[Function]} + query={ + Query { + "ast": _AST { + "_clauses": Array [], + "_indexedClauses": Object { + "field": Object {}, + "group": Array [], + "is": Object {}, + "term": Array [], + }, + }, + "syntax": Object { + "parse": [Function], + "print": [Function], + "printClause": [Function], + }, + "text": "", + } + } + > + +
+ + AWS CloudWatch + , + }, + Object { + "name": "AWS Security Lake", + "value": "AWS Security Lake", + "view": + AWS Security Lake + , + }, + ], + "type": "field_value_selection", + } + } + index={0} + onChange={[Function]} + query={ + Query { + "ast": _AST { + "_clauses": Array [], + "_indexedClauses": Object { + "field": Object {}, + "group": Array [], + "is": Object {}, + "term": Array [], + }, + }, + "syntax": Object { + "parse": [Function], + "print": [Function], + "printClause": [Function], + }, + "text": "", + } + } + > + + Type + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="field_value_selection_0" + isOpen={false} + ownFocus={true} + panelClassName="euiFilterGroup__popoverPanel" + panelPaddingSize="none" + > +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + Type + + + + + + + + + + + + Description + + + + + + + + + + + + Related connections + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+
+
+ Data source +
+
+ + + Connection 1 + +
+
+
+ Type +
+
+ + AWS CloudWatch + +
+
+
+ +
+
+
+ Related connections +
+
+
+
+ + +
+ +
+
+ + +
+
+
+
+
+ Data source +
+
+ + + Connection 2 + +
+
+
+ Type +
+
+ + AWS Security Lake + +
+
+
+ +
+
+
+ Related connections +
+
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+ +
+ +
+ + + +`; + exports[`ManageDirectQueryDataConnectionsTable should get direct query connections failed should render empty table 1`] = ` { expect(component.find(confirmModalIdentifier).exists()).toBe(false); }); }); + + describe('fetch security lake and cloudwatch direct query connections', () => { + beforeEach(async () => { + spyOn(utils, 'getDataConnections').and.returnValue( + Promise.resolve([ + { + type: 'data-connection', + id: 'connection1', + attributes: { + connectionId: 'Connection 1', + type: DataConnectionType.CloudWatch, + }, + }, + { + type: 'data-connection', + id: 'connection2', + attributes: { + connectionId: 'Connection 2', + type: DataConnectionType.SecurityLake, + }, + }, + ]) + ); + + await act(async () => { + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + }); + component.update(); + }); + + it('should get security lake and cloudwatch correctly correctly', async () => { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + component.update(); + const tableRows = component.find('EuiInMemoryTable').prop('items'); + expect(tableRows).toContainEqual( + expect.objectContaining({ + id: 'connection1', + title: 'Connection 1', + type: DataConnectionType.CloudWatch, + }) + ); + expect(tableRows).toContainEqual( + expect.objectContaining({ + id: 'connection2', + title: 'Connection 2', + type: DataConnectionType.SecurityLake, + }) + ); + }); + + it('should render normally', () => { + expect(component).toMatchSnapshot(); + expect(utils.getDataConnections).toHaveBeenCalled(); + }); + + it('should handle errors correctly', async () => { + jest + .spyOn(utils, 'getDataConnections') + .mockImplementation(() => Promise.reject(new Error('Failed to fetch connections'))); + + await act(async () => { + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + }); + component.update(); + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + component.update(); + + const tableRows = component.find('EuiInMemoryTable').prop('items'); + expect(tableRows).toEqual( + expect.not.arrayContaining([ + expect.objectContaining({ objectType: DATA_CONNECTION_SAVED_OBJECT_TYPE }), + ]) + ); + }); + test('should render empty table', () => { + expect(component).toMatchSnapshot(); + }); + }); +}); + +describe('FetchDirectQueryConnections', () => { + let fetchDirectQueryConnections: () => Promise; + let mockSavedObjectsClient: jest.Mocked; + beforeEach(() => { + jest.resetAllMocks(); + (utils.getDataConnections as jest.Mock) = jest.fn(); + + fetchDirectQueryConnections = async (): Promise => { + try { + const dataConnectionSavedObjects = await utils.getDataConnections(mockSavedObjectsClient); + return dataConnectionSavedObjects.map((obj) => ({ + id: obj.id, + title: obj.attributes.connectionId, + type: obj.attributes.type, + objectType: 'data-connection', + })); + } catch (error) { + return []; + } + }; + }); + + it('should return mapped data connections when successful', async () => { + const mockSavedObjects = [ + { + id: 'connection1', + attributes: { connectionId: 'Connection 1', type: DataConnectionType.CloudWatch }, + }, + { + id: 'connection2', + attributes: { connectionId: 'Connection 2', type: DataConnectionType.SecurityLake }, + }, + ]; + + mockSavedObjectsClient = ({ + find: jest.fn().mockResolvedValue({ savedObjects: mockSavedObjects }), + } as unknown) as jest.Mocked; + + (utils.getDataConnections as jest.Mock).mockResolvedValue(mockSavedObjects); + + const result = await fetchDirectQueryConnections(); + + expect(utils.getDataConnections).toHaveBeenCalledWith(mockSavedObjectsClient); + expect(result).toEqual([ + { + id: 'connection1', + title: 'Connection 1', + type: DataConnectionType.CloudWatch, + objectType: 'data-connection', + }, + { + id: 'connection2', + title: 'Connection 2', + type: DataConnectionType.SecurityLake, + objectType: 'data-connection', + }, + ]); + }); + + it('should return an empty array when there is an error', async () => { + mockSavedObjectsClient = ({ + find: jest.fn().mockRejectedValue(new Error('Failed to fetch data connections')), + } as unknown) as jest.Mocked; + + (utils.getDataConnections as jest.Mock).mockRejectedValue( + new Error('Failed to fetch data connections') + ); + + const result = await fetchDirectQueryConnections(); + + expect(utils.getDataConnections).toHaveBeenCalledWith(mockSavedObjectsClient); + expect(result).toEqual([]); + }); +}); + +describe('GetDataConnections', () => { + beforeEach(() => { + jest.resetAllMocks(); + (utils.getDataConnections as jest.Mock) = jest.fn(); + }); + + it('should fetch data connections correctly', async () => { + const mockSavedObjects = [ + { + id: 'connection1', + attributes: { + connectionId: 'Connection 1', + type: DataConnectionType.CloudWatch, + }, + }, + { + id: 'connection2', + attributes: { + connectionId: 'Connection 2', + type: DataConnectionType.SecurityLake, + }, + }, + ]; + + const mockSavedObjectsClient = ({ + find: jest.fn().mockResolvedValue({ + savedObjects: mockSavedObjects, + }), + } as unknown) as SavedObjectsClientContract; + + (utils.getDataConnections as jest.Mock).mockResolvedValue(mockSavedObjects); + + const result = await utils.getDataConnections(mockSavedObjectsClient); + + expect(utils.getDataConnections).toHaveBeenCalledWith(mockSavedObjectsClient); + expect(result).toEqual(mockSavedObjects); + }); + + it('should handle errors when fetching data connections', async () => { + const mockSavedObjectsClient = ({ + find: jest.fn().mockRejectedValue(new Error('Failed to fetch data connections')), + } as unknown) as SavedObjectsClientContract; + + (utils.getDataConnections as jest.Mock).mockRejectedValue( + new Error('Failed to fetch data connections') + ); + + await expect(utils.getDataConnections(mockSavedObjectsClient)).rejects.toThrow( + 'Failed to fetch data connections' + ); + }); }); diff --git a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.tsx b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.tsx index e34483eed75f..c96d846c29c3 100644 --- a/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.tsx +++ b/src/plugins/data_source_management/public/components/direct_query_data_sources_components/direct_query_data_connection/manage_direct_query_data_connections_table.tsx @@ -41,11 +41,13 @@ import { deleteMultipleDataSources, setFirstDataSourceAsDefault, getHideLocalCluster, + getDataConnections, } from '../../utils'; import { LoadingMask } from '../../loading_mask'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DATACONNECTIONS_BASE, LOCAL_CLUSTER } from '../../../constants'; -import { DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../constants'; +import { DatasourceTypeToDisplayName, DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../constants'; +import { DataConnectionType } from '../../../../../data_source/common'; interface DirectQueryDataConnectionsProps extends RouteComponentProps { featureFlagStatus: boolean; @@ -128,50 +130,80 @@ export const ManageDirectQueryDataConnectionsTable = ({ const fetchDataSources = useCallback(() => { setIsLoading(true); - const fetchConnections = featureFlagStatus - ? getDataSources(savedObjects.client) - : http.get(`${DATACONNECTIONS_BASE}`); - - return fetchConnections - .then((response) => { - return featureFlagStatus - ? fetchDataSourceConnections( - response, - http, - notifications, - true, - getHideLocalCluster().enabled - ) - : response.map((dataConnection: DirectQueryDatasourceDetails) => ({ - id: dataConnection.name, - title: dataConnection.name, - type: - { - S3GLUE: 'Amazon S3', - PROMETHEUS: 'Prometheus', - }[dataConnection.connector] || dataConnection.connector, - connectionType: dataConnection.connector, - description: dataConnection.description, - })); - }) - .then((finalData) => { - setData( - featureFlagStatus + const fetchOpenSearchConnections = async (): Promise => { + const fetchConnections = featureFlagStatus + ? getDataSources(savedObjects.client) + : http.get(`${DATACONNECTIONS_BASE}`); + + return fetchConnections + .then((response) => { + return featureFlagStatus + ? fetchDataSourceConnections( + response, + http, + notifications, + true, + getHideLocalCluster().enabled + ) + : response.map((dataConnection: DirectQueryDatasourceDetails) => ({ + id: dataConnection.name, + title: dataConnection.name, + type: + { + S3GLUE: DatasourceTypeToDisplayName.S3GLUE, + PROMETHEUS: DatasourceTypeToDisplayName.PROMETHEUS, + }[dataConnection.connector] || dataConnection.connector, + connectionType: dataConnection.connector, + description: dataConnection.description, + })); + }) + .then((finalData) => { + return featureFlagStatus ? finalData.filter((item) => item.relatedConnections?.length > 0) - : finalData - ); - }) - .catch(() => { - setData([]); + : finalData; + }) + .catch(() => { + notifications.toasts.addDanger( + i18n.translate('dataSourcesManagement.directQueryTable.fetchDataSources', { + defaultMessage: 'Could not fetch data sources', + }) + ); + return []; + }); + }; + + const fetchDirectQueryConnections = async (): Promise => { + try { + const dataConnectionSavedObjects = await getDataConnections(savedObjects.client); + return dataConnectionSavedObjects.map((obj) => ({ + id: obj.id, + title: obj.attributes.connectionId, + type: obj.attributes.type, + })); + } catch (error: any) { + return []; + } + }; + + const fetchAllData = async () => { + try { + const [openSearchConnections, directQueryConnections] = await Promise.all([ + fetchOpenSearchConnections(), + fetchDirectQueryConnections(), + ]); + setData([...openSearchConnections, ...directQueryConnections]); + } catch (error) { notifications.toasts.addDanger( - i18n.translate('dataSourcesManagement.directQueryTable.fetchDataSources', { - defaultMessage: 'Could not fetch data sources', + i18n.translate('dataSourcesManagement.directQueryTable.fetchAllConnections', { + defaultMessage: 'Could not fetch OpenSearch and Direct Query Connections', }) ); - }) - .finally(() => { + } finally { setIsLoading(false); - }); + } + }; + + fetchAllData(); }, [http, savedObjects, notifications, featureFlagStatus]); /* Delete selected data sources*/ @@ -385,6 +417,13 @@ export const ManageDirectQueryDataConnectionsTable = ({ record.connectionType !== DataSourceConnectionType.OpenSearchConnection ? { marginLeft: '20px' } : {}; + if ( + record.type === DataConnectionType.SecurityLake || + record.type === DataConnectionType.CloudWatch + ) { + // TODO: link to details page for security lake and cloudwatch + return {name}; + } return ( { const endpoint = `${DATACONNECTIONS_BASE}/dataSourceMDSId=${dataSourceId}`; @@ -50,8 +51,8 @@ export const getDirectQueryConnections = async (dataSourceId: string, http: Http title: dataConnection.name, type: { - S3GLUE: 'Amazon S3', - PROMETHEUS: 'Prometheus', + S3GLUE: DatasourceTypeToDisplayName.S3GLUE, + PROMETHEUS: DatasourceTypeToDisplayName.PROMETHEUS, }[dataConnection.connector] || dataConnection.connector, connectionType: DataSourceConnectionType.DirectQueryConnection, description: dataConnection.description, @@ -181,6 +182,17 @@ export async function getDataSources(savedObjectsClient: SavedObjectsClientContr ); } +export async function getDataConnections(savedObjectsClient: SavedObjectsClientContract) { + return savedObjectsClient + .find({ + type: 'data-connection', + perPage: 10000, + }) + .then((response) => { + return response?.savedObjects ?? []; + }); +} + export async function getDataSourcesWithFields( savedObjectsClient: SavedObjectsClientContract, fields: string[]