diff --git a/changelogs/fragments/8999.yml b/changelogs/fragments/8999.yml new file mode 100644 index 000000000000..8c6ef11a40e4 --- /dev/null +++ b/changelogs/fragments/8999.yml @@ -0,0 +1,2 @@ +feat: +- Add two-steps loading for associating data sources ([#8999](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8999)) \ No newline at end of file diff --git a/src/plugins/workspace/public/components/data_source_association/association_data_source_modal.test.tsx b/src/plugins/workspace/public/components/data_source_association/association_data_source_modal.test.tsx index 9e50258ae9b8..9ca7049f178a 100644 --- a/src/plugins/workspace/public/components/data_source_association/association_data_source_modal.test.tsx +++ b/src/plugins/workspace/public/components/data_source_association/association_data_source_modal.test.tsx @@ -15,56 +15,80 @@ import { AssociationDataSourceModalProps, } from './association_data_source_modal'; import { AssociationDataSourceModalMode } from 'src/plugins/workspace/common/constants'; +import { DataSourceEngineType } from '../../../../data_source/common/data_sources'; +const dataSourcesList = [ + { + id: 'ds1', + title: 'Data Source 1', + description: 'Description of data source 1', + auth: '', + dataSourceEngineType: '' as DataSourceEngineType, + workspaces: [], + // This is used for mocking saved object function + get: () => { + return 'Data Source 1'; + }, + }, + { + id: 'dqs1', + title: 'Data Connection 1', + description: 'Description of data connection 1', + auth: '', + dataSourceEngineType: '' as DataSourceEngineType, + workspaces: [], + get: () => { + return 'Data Connection 1'; + }, + }, +]; -const setupAssociationDataSourceModal = ({ - mode, - excludedConnectionIds, - handleAssignDataSourceConnections, -}: Partial = {}) => { - const coreServices = coreMock.createStart(); - jest.spyOn(utilsExports, 'getDataSourcesList').mockResolvedValue([]); - jest.spyOn(utilsExports, 'fetchDataSourceConnections').mockResolvedValue([ +const openSearchAndDataConnectionsMock = { + openSearchConnections: [ { id: 'ds1', name: 'Data Source 1', - connectionType: DataSourceConnectionType.OpenSearchConnection, type: 'OpenSearch', - relatedConnections: [ - { - id: 'ds1-dqc1', - name: 'dqc1', - parentId: 'ds1', - connectionType: DataSourceConnectionType.DirectQueryConnection, - type: 'Amazon S3', - }, - ], - }, - { - id: 'ds1-dqc1', - name: 'dqc1', - parentId: 'ds1', - connectionType: DataSourceConnectionType.DirectQueryConnection, - type: 'Amazon S3', - }, - { - id: 'ds2', - name: 'Data Source 2', connectionType: DataSourceConnectionType.OpenSearchConnection, - type: 'OpenSearch', + relatedConnections: [], }, + ], + dataConnections: [ { id: 'dqs1', name: 'Data Connection 1', connectionType: DataSourceConnectionType.DataConnection, type: 'AWS Security Lake', }, + ], +}; +const setupAssociationDataSourceModal = ({ + mode, + excludedConnectionIds, + handleAssignDataSourceConnections, +}: Partial = {}) => { + const coreServices = coreMock.createStart(); + jest.spyOn(utilsExports, 'getDataSourcesList').mockResolvedValue(dataSourcesList); + + jest + .spyOn(utilsExports, 'convertDataSourcesToOpenSearchAndDataConnections') + .mockReturnValue(openSearchAndDataConnectionsMock); + + jest.spyOn(utilsExports, 'fetchDirectQueryConnectionsByIDs').mockResolvedValue([ + { + id: 'ds1-dqc1', + name: 'dqc1', + type: 'Amazon S3', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', + }, ]); + const { logos } = chromeServiceMock.createStartContract(); render( { }); it('should display opensearch connections', async () => { - setupAssociationDataSourceModal(); + setupAssociationDataSourceModal({ mode: AssociationDataSourceModalMode.OpenSearchConnections }); expect(screen.getByText('Associate OpenSearch data sources')).toBeInTheDocument(); expect( screen.getByText( 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query data sources, they will also be available in the workspace.' ) ).toBeInTheDocument(); - await waitFor(() => { - expect(screen.getByRole('option', { name: 'Data Source 1' })).toBeInTheDocument(); - expect(screen.getByRole('option', { name: 'Data Source 2' })).toBeInTheDocument(); - }); + await waitFor(() => expect(screen.getByText('Data Source 1')).toBeInTheDocument()); }); it('should display direct query connections after opensearch connection selected', async () => { @@ -127,6 +148,7 @@ describe('AssociationDataSourceModal', () => { }); expect(screen.getByText('Associate direct query data sources')).toBeInTheDocument(); await waitFor(() => { + expect(screen.getByText('Data Source 1')).toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'dqc1' })).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('option', { name: 'Data Source 1' })); expect(screen.getByRole('option', { name: 'dqc1' })).toBeInTheDocument(); @@ -135,17 +157,14 @@ describe('AssociationDataSourceModal', () => { it('should hide associated connections', async () => { setupAssociationDataSourceModal({ - excludedConnectionIds: ['ds2'], + excludedConnectionIds: ['ds1'], }); expect( screen.getByText( 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query data sources, they will also be available in the workspace.' ) ).toBeInTheDocument(); - await waitFor(() => { - expect(screen.getByRole('option', { name: 'Data Source 1' })).toBeInTheDocument(); - expect(screen.queryByRole('option', { name: 'Data Source 2' })).not.toBeInTheDocument(); - }); + expect(screen.queryByRole('option', { name: 'Data Source 1' })).not.toBeInTheDocument(); }); it('should call handleAssignDataSourceConnections with opensearch connections after assigned', async () => { @@ -153,12 +172,14 @@ describe('AssociationDataSourceModal', () => { setupAssociationDataSourceModal({ handleAssignDataSourceConnections: handleAssignDataSourceConnectionsMock, }); - await waitFor(() => { - fireEvent.click(screen.getByRole('option', { name: 'Data Source 1' })); - fireEvent.click(screen.getByRole('button', { name: 'Associate data sources' })); + expect(screen.getByText('Data Source 1')).toBeInTheDocument(); + expect(screen.getByText('Associate data sources')).toBeInTheDocument(); }); + fireEvent.click(screen.getByText('Data Source 1')); + fireEvent.click(screen.getByText('Associate data sources')); + expect(handleAssignDataSourceConnectionsMock).toHaveBeenCalledWith([ { id: 'ds1', @@ -182,14 +203,17 @@ describe('AssociationDataSourceModal', () => { const handleAssignDataSourceConnectionsMock = jest.fn(); setupAssociationDataSourceModal({ handleAssignDataSourceConnections: handleAssignDataSourceConnectionsMock, - mode: AssociationDataSourceModalMode.DirectQueryConnections, + mode: AssociationDataSourceModalMode.OpenSearchConnections, }); - await waitFor(() => { - fireEvent.click(screen.getByRole('option', { name: 'Data Connection 1' })); - fireEvent.click(screen.getByRole('button', { name: 'Associate data sources' })); + expect(screen.getByText('Data Source 1')).toBeInTheDocument(); + expect(screen.getByText('Data Connection 1')).toBeInTheDocument(); }); + expect(screen.getByText('Associate data sources')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Data Connection 1')); + fireEvent.click(screen.getByText('Associate data sources')); + expect(handleAssignDataSourceConnectionsMock).toHaveBeenCalledWith([ { id: 'dqs1', diff --git a/src/plugins/workspace/public/components/data_source_association/association_data_source_modal.tsx b/src/plugins/workspace/public/components/data_source_association/association_data_source_modal.tsx index 8c093cdf46fa..cecb7733dffe 100644 --- a/src/plugins/workspace/public/components/data_source_association/association_data_source_modal.tsx +++ b/src/plugins/workspace/public/components/data_source_association/association_data_source_modal.tsx @@ -25,7 +25,11 @@ import { import { FormattedMessage } from 'react-intl'; import { i18n } from '@osd/i18n'; -import { getDataSourcesList, fetchDataSourceConnections } from '../../utils'; +import { + getDataSourcesList, + fetchDirectQueryConnectionsByIDs, + convertDataSourcesToOpenSearchAndDataConnections, +} from '../../utils'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { HttpStart, NotificationsStart, SavedObjectsStart } from '../../../../../core/public'; import { AssociationDataSourceModalMode } from '../../../common/constants'; @@ -88,42 +92,39 @@ const convertConnectionToOption = ({ connection, selectedConnectionIds, logos, + showDirectQueryConnections, }: { connection: DataSourceConnection; selectedConnectionIds: string[]; logos: Logos; -}) => ({ - label: connection.name, - key: connection.id, - description: connection.description, - append: - connection.relatedConnections && connection.relatedConnections.length > 0 ? ( - - {i18n.translate('workspace.form.selectDataSource.optionBadge', { - defaultMessage: '+ {relatedConnections} related', - values: { - relatedConnections: connection.relatedConnections.length, - }, - })} - - ) : undefined, - disabled: connection.connectionType === DataSourceConnectionType.DirectQueryConnection, - checked: - connection.connectionType !== DataSourceConnectionType.DirectQueryConnection && - selectedConnectionIds.includes(connection.id) - ? ('on' as const) - : undefined, - prepend: - connection.connectionType === DataSourceConnectionType.DirectQueryConnection ? ( - <> -
- - - ) : ( - - ), - parentId: connection.parentId, -}); + showDirectQueryConnections: boolean; +}) => { + return { + label: connection.name, + key: connection.id, + description: connection.description, + append: showDirectQueryConnections && + connection.relatedConnections && + connection.relatedConnections.length > 0 && ( + + {i18n.translate('workspace.form.selectDataSource.optionBadge', { + defaultMessage: '+ {relatedConnections} related', + values: { + relatedConnections: connection.relatedConnections.length, + }, + })} + + ), + disabled: connection.connectionType === DataSourceConnectionType.DirectQueryConnection, + checked: + connection.connectionType !== DataSourceConnectionType.DirectQueryConnection && + selectedConnectionIds.includes(connection.id) + ? ('on' as const) + : undefined, + prepend: , + parentId: connection.parentId, + }; +}; const convertConnectionsToOptions = ({ connections, @@ -148,24 +149,33 @@ const convertConnectionsToOptions = ({ } if (connection.connectionType === DataSourceConnectionType.DataConnection) { - if (showDirectQueryConnections) { + if (!showDirectQueryConnections) { return [connection]; } return []; } - if (showDirectQueryConnections) { - if (!connection.relatedConnections || connection.relatedConnections.length === 0) { - return []; + if (connection.connectionType === DataSourceConnectionType.OpenSearchConnection) { + if (showDirectQueryConnections) { + return [ + connection, + ...(selectedConnectionIds.includes(connection.id) + ? connection.relatedConnections ?? [] + : []), + ]; } - return [ - connection, - ...(selectedConnectionIds.includes(connection.id) ? connection.relatedConnections : []), - ]; } + return [connection]; }) - .map((connection) => convertConnectionToOption({ connection, selectedConnectionIds, logos })); + .map((connection) => + convertConnectionToOption({ + connection, + selectedConnectionIds, + logos, + showDirectQueryConnections, + }) + ); }; export interface AssociationDataSourceModalProps { @@ -228,17 +238,78 @@ export const AssociationDataSourceModalContent = ({ } }, [selectedConnectionIds, allConnections, handleAssignDataSourceConnections]); + const handleDirectQueryConnections = useCallback( + async ( + openSearchConnections: DataSourceConnection[], + dataConnections: DataSourceConnection[] + ) => { + if (mode === AssociationDataSourceModalMode.OpenSearchConnections) { + return [...openSearchConnections, ...dataConnections]; + } + + const fetchDqcConnectionsPromises = openSearchConnections.map((ds) => + fetchDirectQueryConnectionsByIDs([ds.id], http, notifications) + .then((directQueryConnections) => ({ + id: ds.id, + relatedConnections: directQueryConnections, + })) + .catch(() => ({ + id: ds.id, + relatedConnections: [], + })) + ); + const dqcConnections = await Promise.all(fetchDqcConnectionsPromises); + + const allConnectionsWithDQC = openSearchConnections + .filter((connection) => { + const filteredData = dqcConnections.find((c) => c.id === connection.id); + return filteredData && filteredData.relatedConnections.length > 0; + }) + .map((connection) => { + const relatedDQC = dqcConnections.find((c) => c.id === connection.id); + return { + ...connection, + relatedConnections: relatedDQC?.relatedConnections, + } as DataSourceConnection; + }); + + return allConnectionsWithDQC; + }, + [http, mode, notifications] + ); + useEffect(() => { - setIsLoading(true); - getDataSourcesList(savedObjects.client, ['*']) - .then((dataSourcesList) => fetchDataSourceConnections(dataSourcesList, http, notifications)) - .then((connections) => { + const fetchDataSources = async () => { + const dataSourcesList = await getDataSourcesList(savedObjects.client, ['*']); + const { + openSearchConnections, + dataConnections, + } = convertDataSourcesToOpenSearchAndDataConnections(dataSourcesList); + return { openSearchConnections, dataConnections }; + }; + + const fetchDataSourcesAndHandleRelatedConnections = async () => { + setIsLoading(true); + try { + const { openSearchConnections, dataConnections } = await fetchDataSources(); + const connections = await handleDirectQueryConnections( + openSearchConnections, + dataConnections + ); setAllConnections(connections); - }) - .finally(() => { + } catch { + notifications?.toasts.addDanger( + i18n.translate('workspace.detail.dataSources.associateModal.fetchDataSourcesError', { + defaultMessage: 'Failed to get data sources', + }) + ); + } finally { setIsLoading(false); - }); - }, [savedObjects.client, http, notifications, mode]); + } + }; + + fetchDataSourcesAndHandleRelatedConnections(); + }, [savedObjects.client, notifications, http, mode, handleDirectQueryConnections]); useEffect(() => { setOptions( @@ -250,7 +321,7 @@ export const AssociationDataSourceModalContent = ({ logos, }) ); - }, [allConnections, excludedConnectionIds, selectedConnectionIds, mode, logos]); + }, [excludedConnectionIds, selectedConnectionIds, mode, allConnections, logos]); return ( <> diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx index 9f6dc00ec9a7..4d13c7ca0366 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx @@ -44,31 +44,31 @@ const PublicAPPInfoMap = new Map([ const dataSourcesList = [ { - id: 'id1', - title: 'ds1', + id: 'ds1', + title: 'Data Source 1', description: 'Description of data source 1', auth: '', dataSourceEngineType: '' as DataSourceEngineType, workspaces: [], // This is used for mocking saved object function get: () => { - return 'ds1'; + return 'Data Source 1'; }, }, { - id: 'id2', - title: 'ds2', - description: 'Description of data source 1', + id: 'ds2', + title: 'Data Source 2', + description: 'Description of data source 2', auth: '', dataSourceEngineType: '' as DataSourceEngineType, workspaces: [], get: () => { - return 'ds2'; + return 'Data Source 2'; }, }, { - id: 'id3', - title: 'dqs1', + id: 'ds3', + title: 'Data connection 1', description: 'Description of data connection 1', auth: '', dataSourceEngineType: '' as DataSourceEngineType, @@ -81,36 +81,69 @@ const dataSourcesList = [ }, ]; +const directQueryConnectionsMock = [ + { + id: 'ds1-dqc1', + name: 'dqc1', + parentId: 'ds1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + type: 'Amazon S3', + }, +]; const dataSourceConnectionsList = [ { - id: 'id1', - name: 'ds1', + id: 'ds1', + name: 'Data Source 1', connectionType: DataSourceConnectionType.OpenSearchConnection, type: 'OpenSearch', relatedConnections: [], }, { - id: 'id2', - name: 'ds2', + id: 'ds2', + name: 'Data Source 2', connectionType: DataSourceConnectionType.OpenSearchConnection, type: 'OpenSearch', }, +]; + +const dataConnectionsList = [ { - id: 'id3', - name: 'dqs1', + id: 'ds3', + name: 'Data connection 1', description: 'Description of data connection 1', connectionType: DataSourceConnectionType.DataConnection, type: 'AWS Security Lake', }, ]; -const mockCoreStart = coreMock.createStart(); -jest.spyOn(utils, 'fetchDataSourceConnections').mockImplementation(async (passedDataSources) => { - return dataSourceConnectionsList.filter(({ id }) => - passedDataSources.some((dataSource) => dataSource.id === id) - ); +jest.spyOn(utils, 'convertDataSourcesToOpenSearchAndDataConnections').mockReturnValue({ + openSearchConnections: [...dataSourceConnectionsList], + dataConnections: [...dataConnectionsList], }); +jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSourcesList); +jest.spyOn(utils, 'fulfillRelatedConnections').mockReturnValue([ + { + id: 'ds1', + name: 'Data Source 1', + type: 'OpenSearch', + connectionType: 0, + relatedConnections: [ + { + id: 'ds1-dqc1', + name: 'dqc1', + type: 'Amazon S3', + connectionType: 1, + parentId: 'ds1', + }, + ], + }, +]); + +jest.spyOn(utils, 'fetchDirectQueryConnectionsByIDs').mockResolvedValue(directQueryConnectionsMock); + +const mockCoreStart = coreMock.createStart(); + const WorkspaceCreator = ({ isDashboardAdmin = false, dataSourceEnabled = false, @@ -382,7 +415,7 @@ describe('WorkspaceCreator', () => { }), expect.objectContaining({ dataConnections: [], - dataSources: ['id1'], + dataSources: ['ds1'], }) ); await waitFor(() => { @@ -413,13 +446,14 @@ describe('WorkspaceCreator', () => { target: { value: 'test workspace name' }, }); fireEvent.click(getByTestId('workspaceUseCase-observability')); - fireEvent.click(getByTestId('workspace-creator-dqc-assign-button')); + fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); + expect( + getByText( + 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query data sources, they will also be available in the workspace.' + ) + ).toBeInTheDocument(); + await waitFor(() => { - expect( - getByText( - 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query data sources, they will also be available in the workspace.' - ) - ).toBeInTheDocument(); expect(getByText(dataSourcesList[2].title)).toBeInTheDocument(); }); fireEvent.click(getByText(dataSourcesList[2].title)); @@ -431,7 +465,7 @@ describe('WorkspaceCreator', () => { name: 'test workspace name', }), expect.objectContaining({ - dataConnections: ['id3'], + dataConnections: ['ds3'], dataSources: [], }) ); diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.test.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.test.tsx index c648a5203438..bf6f4705d57e 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.test.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.test.tsx @@ -14,22 +14,7 @@ import { DataSourceConnectionType } from '../../../common/types'; import { SelectDataSourcePanel, SelectDataSourcePanelProps } from './select_data_source_panel'; -const dataSourceConnectionsMock = [ - { - id: 'ds1', - name: 'Data Source 1', - connectionType: DataSourceConnectionType.OpenSearchConnection, - type: 'OpenSearch', - relatedConnections: [ - { - id: 'ds1-dqc1', - name: 'dqc1', - parentId: 'ds1', - connectionType: DataSourceConnectionType.DirectQueryConnection, - type: 'Amazon S3', - }, - ], - }, +const directQueryConnectionsMock = [ { id: 'ds1-dqc1', name: 'dqc1', @@ -37,6 +22,15 @@ const dataSourceConnectionsMock = [ connectionType: DataSourceConnectionType.DirectQueryConnection, type: 'Amazon S3', }, +]; +const dataSourceConnectionsMock = [ + { + id: 'ds1', + name: 'Data Source 1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', + relatedConnections: [], + }, { id: 'ds2', name: 'Data Source 2', @@ -45,8 +39,6 @@ const dataSourceConnectionsMock = [ }, ]; -const assignedDataSourcesConnections = [dataSourceConnectionsMock[0], dataSourceConnectionsMock[2]]; - const dataSources = [ { id: 'ds1', @@ -65,13 +57,13 @@ const dataSources = [ workspaces: [], }, ]; - jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSources); -jest.spyOn(utils, 'fetchDataSourceConnections').mockImplementation(async (passedDataSources) => { - return dataSourceConnectionsMock.filter(({ id }) => - passedDataSources.some((dataSource) => dataSource.id === id) - ); -}); + +jest + .spyOn(utils, 'convertDataSourcesToOpenSearchAndDataConnections') + .mockReturnValue({ openSearchConnections: [...dataSourceConnectionsMock], dataConnections: [] }); + +jest.spyOn(utils, 'fetchDirectQueryConnectionsByIDs').mockResolvedValue(directQueryConnectionsMock); const mockCoreStart = coreMock.createStart(); @@ -126,77 +118,58 @@ describe('SelectDataSourcePanel', () => { ); }); it('should render consistent data sources when selected data sources passed', async () => { - const { getByText, getByTestId, queryByText } = setup({ - assignedDataSourceConnections: [assignedDataSourcesConnections[0]], - }); - - await waitFor(() => { - expect(getByText(assignedDataSourcesConnections[0].name)).toBeInTheDocument(); - expect(queryByText(assignedDataSourcesConnections[1].name)).not.toBeInTheDocument(); + const onChangeMock = jest.fn(); + const { getByTestId, getByText, queryByText } = setup({ + onChange: onChangeMock, + assignedDataSourceConnections: [dataSourceConnectionsMock[0]], }); + expect(queryByText('Data Source 1')).toBeInTheDocument(); + expect(queryByText('Data Source 2')).not.toBeInTheDocument(); + expect(onChangeMock).not.toHaveBeenCalled(); fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); - - await waitFor(() => { - expect(getByText(assignedDataSourcesConnections[1].name)).toBeInTheDocument(); - }); + expect( + getByText( + 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query data sources, they will also be available in the workspace.' + ) + ).toBeInTheDocument(); }); it('should call onChange when updating data sources', async () => { const onChangeMock = jest.fn(); - const { getByTestId, getByText } = setup({ + const { getByTestId, getByText, findByText } = setup({ onChange: onChangeMock, assignedDataSourceConnections: [], }); expect(onChangeMock).not.toHaveBeenCalled(); fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); - - await waitFor(() => { - expect( - getByText( - 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query data sources, they will also be available in the workspace.' - ) - ).toBeInTheDocument(); - expect(getByText(assignedDataSourcesConnections[1].name)).toBeInTheDocument(); - }); - - fireEvent.click(getByText(assignedDataSourcesConnections[1].name)); - fireEvent.click(getByText('Associate data sources')); - expect(onChangeMock).toHaveBeenCalledWith([ - expect.objectContaining({ - id: assignedDataSourcesConnections[1].id, - }), - ]); - - fireEvent.click(getByTestId('workspace-creator-dqc-assign-button')); - await waitFor(() => { - expect(getByText(assignedDataSourcesConnections[0].name)).toBeInTheDocument(); - }); - fireEvent.click(getByText(assignedDataSourcesConnections[0].name)); + expect( + getByText( + 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query data sources, they will also be available in the workspace.' + ) + ).toBeInTheDocument(); + await findByText('Data Source 1'); + fireEvent.click(getByText('Data Source 1')); fireEvent.click(getByText('Associate data sources')); - expect(onChangeMock).toHaveBeenCalledWith([ - expect.objectContaining({ - id: assignedDataSourcesConnections[0].id, - }), - ]); + expect(onChangeMock).toHaveBeenCalledWith([expect.objectContaining({ id: 'ds1' })]); }); it('should call onChange when deleting selected data source', async () => { const onChangeMock = jest.fn(); const { getByText, getByTestId } = setup({ onChange: onChangeMock, - assignedDataSourceConnections: assignedDataSourcesConnections, + assignedDataSourceConnections: dataSourceConnectionsMock, }); fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); await waitFor(() => { - expect(getByText(assignedDataSourcesConnections[0].name)).toBeInTheDocument(); - expect(getByText(assignedDataSourcesConnections[1].name)).toBeInTheDocument(); + expect(getByText(dataSourceConnectionsMock[0].name)).toBeInTheDocument(); + expect(getByText(dataSourceConnectionsMock[1].name)).toBeInTheDocument(); }); - fireEvent.click(getByText(assignedDataSourcesConnections[0].name)); - fireEvent.click(getByText(assignedDataSourcesConnections[1].name)); + fireEvent.click(getByText(dataSourceConnectionsMock[0].name)); + fireEvent.click(getByText(dataSourceConnectionsMock[1].name)); expect(onChangeMock).not.toHaveBeenCalled(); @@ -206,7 +179,7 @@ describe('SelectDataSourcePanel', () => { fireEvent.click(getByTestId('checkboxSelectRow-' + dataSources[1].id)); fireEvent.click(getByText('Remove selected')); }); - expect(onChangeMock).toHaveBeenCalledWith([assignedDataSourcesConnections[0]]); + expect(onChangeMock).toHaveBeenCalledWith([dataSourceConnectionsMock[0]]); }); it('should close associate data sources modal', async () => { diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts index 9e717ba00c57..d95914846278 100644 --- a/src/plugins/workspace/public/utils.test.ts +++ b/src/plugins/workspace/public/utils.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AppNavLinkStatus, ChromeNavLink, NavGroupType, PublicAppInfo } from '../../../core/public'; +import { AppNavLinkStatus, NavGroupType, PublicAppInfo } from '../../../core/public'; import { featureMatchesConfig, filterWorkspaceConfigurableApps, @@ -16,6 +16,7 @@ import { prependWorkspaceToBreadcrumbs, getIsOnlyAllowEssentialUseCase, mergeDataSourcesWithConnections, + fetchDirectQueryConnectionsByIDs, getUseCaseUrl, } from './utils'; import { WorkspaceAvailability } from '../../../core/public'; @@ -331,6 +332,36 @@ describe('workspace utils: filterWorkspaceConfigurableApps', () => { }); }); +describe('workspace utils: fetchDirectQueryConnectionsByIDs', () => { + it('should successfully retrieve direct query connections by Ids', async () => { + const coreStart = coreMock.createStart(); + const httpMock = coreStart.http; + const notificationsMock = coreStart.notifications; + httpMock.get.mockResolvedValue([ + { name: 'Connection A', description: 'Test A', connector: 'S3GLUE' }, + ]); + + const dataSourceIds = ['1']; + const result = await fetchDirectQueryConnectionsByIDs( + dataSourceIds, + httpMock, + notificationsMock + ); + expect(result).toEqual([ + { + id: '1-Connection A', + name: 'Connection A', + parentId: '1', + type: 'Amazon S3', + connectionType: DataSourceConnectionType.DirectQueryConnection, + description: 'Test A', + }, + ]); + expect(httpMock.get).toHaveBeenCalledWith(expect.stringContaining('dataSourceMDSId=1')); + expect(notificationsMock.toasts.addDanger).not.toHaveBeenCalled(); + }); +}); + describe('workspace utils: isFeatureIdInsideUseCase', () => { it('should return false for invalid use case', () => { expect(isFeatureIdInsideUseCase('discover', 'invalid', [])).toBe(false); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 5eda2c887749..c0225a571367 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -539,6 +539,29 @@ export const fetchDataSourceConnections = async ( } }; +export const fetchDirectQueryConnectionsByIDs = async ( + dataSourceIds: string[], + http: HttpSetup | undefined, + notifications: NotificationsStart | undefined +) => { + try { + const directQueryConnections = await fetchDataSourceConnectionsByDataSourceIds( + // Only data source saved object type needs to fetch data source connections, data connection type object not. + dataSourceIds, + http + ); + + return directQueryConnections.sort((a, b) => a.name.localeCompare(b.name)); + } catch (error) { + notifications?.toasts.addDanger( + i18n.translate('workspace.detail.dataSources.error.message', { + defaultMessage: 'Cannot fetch direct query connections', + }) + ); + return []; + } +}; + export const getUseCase = (workspace: WorkspaceObject, availableUseCases: WorkspaceUseCase[]) => { if (!workspace.features) { return;