From 1ed0922d23fe20d2974c1ca0bacb801238cf6380 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Tue, 3 Dec 2024 15:53:46 +0800 Subject: [PATCH 1/7] add 2 step loading Signed-off-by: Qxisylolo --- .../association_data_source_modal.test.tsx | 92 ++++++----- .../association_data_source_modal.tsx | 154 ++++++++++++------ src/plugins/workspace/public/utils.ts | 54 ++++++ 3 files changed, 211 insertions(+), 89 deletions(-) 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..56a8cacefb3c 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 @@ -6,7 +6,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { IntlProvider } from 'react-intl'; -import { DataSourceConnectionType } from '../../../common/types'; +import { DataSourceConnectionType, DataSourceConnection } from '../../../common/types'; import { chromeServiceMock, coreMock } from '../../../../../core/public/mocks'; import * as utilsExports from '../../utils'; @@ -23,42 +23,63 @@ const setupAssociationDataSourceModal = ({ }: Partial = {}) => { const coreServices = coreMock.createStart(); jest.spyOn(utilsExports, 'getDataSourcesList').mockResolvedValue([]); - jest.spyOn(utilsExports, 'fetchDataSourceConnections').mockResolvedValue([ - { - id: 'ds1', - name: 'Data Source 1', - connectionType: DataSourceConnectionType.OpenSearchConnection, - type: 'OpenSearch', - relatedConnections: [ + + jest.spyOn(utilsExports, 'getOpenSearchAndDataConnections').mockResolvedValue({ + openSearchConnections: [ + { + id: 'ds1', + name: 'Data Source 1', + type: 'OpenSearch', + connectionType: 0, + relatedConnections: [], + }, + ], + dataConnections: [ + { + id: 'dqs1', + name: 'Data Connection 1', + connectionType: DataSourceConnectionType.DataConnection, + type: 'AWS Security Lake', + }, + ], + }); + + jest + .spyOn(utilsExports, 'updateFullFillRelatedConnections') + .mockImplementationOnce( + ( + openSearchConnections: DataSourceConnection[], + directQueryConnections: DataSourceConnection[] + ): DataSourceConnection[] => [ { - id: 'ds1-dqc1', - name: 'dqc1', - parentId: 'ds1', - connectionType: DataSourceConnectionType.DirectQueryConnection, - type: 'Amazon S3', + 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(utilsExports, 'fetchDirectQueryConnections').mockResolvedValue([ { id: 'ds1-dqc1', name: 'dqc1', - parentId: 'ds1', - connectionType: DataSourceConnectionType.DirectQueryConnection, type: 'Amazon S3', - }, - { - id: 'ds2', - name: 'Data Source 2', - connectionType: DataSourceConnectionType.OpenSearchConnection, - type: 'OpenSearch', - }, - { - id: 'dqs1', - name: 'Data Connection 1', - connectionType: DataSourceConnectionType.DataConnection, - type: 'AWS Security Lake', + connectionType: 1, + description: 'direct_query_connections_1', + parentId: 'ds1', }, ]); + const { logos } = chromeServiceMock.createStartContract(); render( @@ -115,10 +136,7 @@ describe('AssociationDataSourceModal', () => { '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 +145,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 +154,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 () => { 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..99bce7150642 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,12 @@ import { import { FormattedMessage } from 'react-intl'; import { i18n } from '@osd/i18n'; -import { getDataSourcesList, fetchDataSourceConnections } from '../../utils'; +import { + getDataSourcesList, + getOpenSearchAndDataConnections, + fetchDirectQueryConnections, + updateFullFillRelatedConnections, +} from '../../utils'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { HttpStart, NotificationsStart, SavedObjectsStart } from '../../../../../core/public'; import { AssociationDataSourceModalMode } from '../../../common/constants'; @@ -88,42 +93,47 @@ const convertConnectionToOption = ({ connection, selectedConnectionIds, logos, + isRelatedConnectionsLoaded, }: { connection: DataSourceConnection; selectedConnectionIds: string[]; logos: Logos; -}) => ({ - label: connection.name, - key: connection.id, - description: connection.description, - append: - connection.relatedConnections && connection.relatedConnections.length > 0 ? ( + isRelatedConnectionsLoaded: boolean; +}) => { + return { + label: connection.name, + key: connection.id, + description: connection.description, + append: isRelatedConnectionsLoaded ? ( + connection.relatedConnections && connection.relatedConnections.length > 0 ? ( + + {i18n.translate('workspace.form.selectDataSource.optionBadge', { + defaultMessage: '+ {relatedConnections} related', + values: { + relatedConnections: connection.relatedConnections.length, + }, + })} + + ) : undefined + ) : ( - {i18n.translate('workspace.form.selectDataSource.optionBadge', { - defaultMessage: '+ {relatedConnections} related', - values: { - relatedConnections: connection.relatedConnections.length, - }, + {i18n.translate('workspace.form.selectDataSource.loading', { + defaultMessage: 'Loading...', })} - ) : 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, -}); + + 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, @@ -131,12 +141,14 @@ const convertConnectionsToOptions = ({ selectedConnectionIds, excludedConnectionIds, logos, + isRelatedConnectionsLoaded, }: { connections: DataSourceConnection[]; excludedConnectionIds: string[]; showDirectQueryConnections: boolean; selectedConnectionIds: string[]; logos: Logos; + isRelatedConnectionsLoaded: boolean; }) => { return connections .flatMap((connection) => { @@ -154,18 +166,28 @@ const convertConnectionsToOptions = ({ return []; } - if (showDirectQueryConnections) { - if (!connection.relatedConnections || connection.relatedConnections.length === 0) { - return []; + if (connection.connectionType === DataSourceConnectionType.OpenSearchConnection) { + if (showDirectQueryConnections) { + if (!connection.relatedConnections || connection.relatedConnections.length === 0) { + return [connection]; + } + 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, + isRelatedConnectionsLoaded, + }) + ); }; export interface AssociationDataSourceModalProps { @@ -199,6 +221,7 @@ export const AssociationDataSourceModalContent = ({ }: AssociationDataSourceModalProps) => { const [allConnections, setAllConnections] = useState([]); const [selectedConnectionIds, setSelectedConnectionIds] = useState([]); + const [isRelatedConnectionsLoaded, setIsRelatedConnectionsLoaded] = useState(false); const [options, setOptions] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); @@ -230,27 +253,56 @@ export const AssociationDataSourceModalContent = ({ useEffect(() => { setIsLoading(true); + setIsRelatedConnectionsLoaded(false); getDataSourcesList(savedObjects.client, ['*']) - .then((dataSourcesList) => fetchDataSourceConnections(dataSourcesList, http, notifications)) - .then((connections) => { - setAllConnections(connections); + .then((dataSourcesList) => { + return getOpenSearchAndDataConnections(dataSourcesList, notifications).then( + ({ openSearchConnections, dataConnections }) => { + setAllConnections([...openSearchConnections, ...dataConnections]); + return { openSearchConnections, dataConnections, dataSourcesList }; + } + ); + }) + .then(({ openSearchConnections, dataConnections, dataSourcesList }) => { + fetchDirectQueryConnections(dataSourcesList, http, notifications).then( + (directQueryConnections) => { + const updatedOpenSearchConnections = updateFullFillRelatedConnections( + openSearchConnections, + directQueryConnections + ); + + setAllConnections([...updatedOpenSearchConnections, ...dataConnections]); + setIsRelatedConnectionsLoaded(true); // related connections are completely loaded + } + ); }) .finally(() => { setIsLoading(false); }); - }, [savedObjects.client, http, notifications, mode]); + }, [savedObjects.client, notifications, http]); useEffect(() => { - setOptions( - convertConnectionsToOptions({ - connections: allConnections, - excludedConnectionIds, - selectedConnectionIds, - showDirectQueryConnections: mode === AssociationDataSourceModalMode.DirectQueryConnections, - logos, - }) - ); - }, [allConnections, excludedConnectionIds, selectedConnectionIds, mode, logos]); + if (allConnections.length > 0) { + setOptions( + convertConnectionsToOptions({ + connections: allConnections, + excludedConnectionIds, + selectedConnectionIds, + showDirectQueryConnections: + mode === AssociationDataSourceModalMode.DirectQueryConnections, + logos, + isRelatedConnectionsLoaded, + }) + ); + } + }, [ + excludedConnectionIds, + selectedConnectionIds, + mode, + allConnections, + logos, + isRelatedConnectionsLoaded, + ]); return ( <> diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 5eda2c887749..666d58e224b7 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -539,6 +539,60 @@ export const fetchDataSourceConnections = async ( } }; +export const updateFullFillRelatedConnections = ( + openSearchConnections: DataSourceConnection[], + directQueryConnections: DataSourceConnection[] +): DataSourceConnection[] => { + return fulfillRelatedConnections(openSearchConnections, directQueryConnections); +}; + +export const fetchDirectQueryConnections = async ( + dataSources: DataSource[], + 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. + dataSources.filter((ds) => ds.type === DATA_SOURCE_SAVED_OBJECT_TYPE).map((ds) => ds.id), + 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 getOpenSearchAndDataConnections = async ( + dataSources: DataSource[] | DataConnection[], + notifications: NotificationsStart | undefined +): Promise<{ + openSearchConnections: DataSourceConnection[]; + dataConnections: DataSourceConnection[]; +}> => { + try { + const { + openSearchConnections, + dataConnections, + } = convertDataSourcesToOpenSearchAndDataConnections(dataSources); + + return { openSearchConnections, dataConnections }; + } catch (error) { + notifications?.toasts.addDanger( + i18n.translate('workspace.detail.dataSources.connections.error.message', { + defaultMessage: 'Cannot fetch OpenSearch connections and data connections ', + }) + ); + return { openSearchConnections: [], dataConnections: [] }; + } +}; + export const getUseCase = (workspace: WorkspaceObject, availableUseCases: WorkspaceUseCase[]) => { if (!workspace.features) { return; From 6c87e659f17f2b616f5663d097bbe866d79021fc Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 07:57:19 +0000 Subject: [PATCH 2/7] Changeset file for PR #8999 created/updated --- changelogs/fragments/8999.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/8999.yml 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 From df5de09b52ed7bb3b832a2b88723b981f9084607 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Wed, 4 Dec 2024 10:18:56 +0800 Subject: [PATCH 3/7] ifx ut Signed-off-by: Qxisylolo --- .../select_data_source_panel.test.tsx | 122 +++++++++--------- 1 file changed, 63 insertions(+), 59 deletions(-) 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..393018db1c83 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 @@ -10,26 +10,11 @@ import { coreMock } from '../../../../../core/public/mocks'; import * as utils from '../../utils'; import { DataSourceEngineType } from 'src/plugins/data_source/common/data_sources'; import { OpenSearchDashboardsContextProvider } from '../../../../../plugins/opensearch_dashboards_react/public'; -import { DataSourceConnectionType } from '../../../common/types'; +import { DataSourceConnectionType, DataSourceConnection } 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', @@ -67,12 +59,38 @@ const dataSources = [ ]; 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, 'getOpenSearchAndDataConnections').mockResolvedValue({ + openSearchConnections: [...dataSourceConnectionsMock], + dataConnections: [], }); +jest + .spyOn(utils, 'updateFullFillRelatedConnections') + .mockImplementationOnce( + ( + openSearchConnections: DataSourceConnection[], + directQueryConnections: DataSourceConnection[] + ): DataSourceConnection[] => [ + { + 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, 'fetchDirectQueryConnections').mockResolvedValue(directQueryConnectionsMock); + const mockCoreStart = coreMock.createStart(); const setup = ({ @@ -126,20 +144,21 @@ 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 () => { @@ -158,45 +177,30 @@ describe('SelectDataSourcePanel', () => { '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(); + expect(getByText('Data Source 1')).toBeInTheDocument(); + expect(getByText('Data Source 2')).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)); + 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 +210,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 () => { From feaba8bde4470f4c55764105f4476f5beff9a7b5 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Wed, 4 Dec 2024 16:56:14 +0800 Subject: [PATCH 4/7] resolve comments Signed-off-by: Qxisylolo --- .../association_data_source_modal.test.tsx | 80 ++++++++--------- .../association_data_source_modal.tsx | 11 ++- .../workspace_creator.test.tsx | 86 ++++++++++++++----- .../select_data_source_panel.test.tsx | 8 +- src/plugins/workspace/public/utils.test.ts | 2 +- src/plugins/workspace/public/utils.ts | 28 ++---- 6 files changed, 120 insertions(+), 95 deletions(-) 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 56a8cacefb3c..785d640c3e82 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 @@ -6,7 +6,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { IntlProvider } from 'react-intl'; -import { DataSourceConnectionType, DataSourceConnection } from '../../../common/types'; +import { DataSourceConnectionType } from '../../../common/types'; import { chromeServiceMock, coreMock } from '../../../../../core/public/mocks'; import * as utilsExports from '../../utils'; @@ -16,6 +16,25 @@ import { } from './association_data_source_modal'; import { AssociationDataSourceModalMode } from 'src/plugins/workspace/common/constants'; +const openSearchAndDataConnectionsMock = { + openSearchConnections: [ + { + id: 'ds1', + name: 'Data Source 1', + type: 'OpenSearch', + connectionType: DataSourceConnectionType.OpenSearchConnection, + relatedConnections: [], + }, + ], + dataConnections: [ + { + id: 'dqs1', + name: 'Data Connection 1', + connectionType: DataSourceConnectionType.DataConnection, + type: 'AWS Security Lake', + }, + ], +}; const setupAssociationDataSourceModal = ({ mode, excludedConnectionIds, @@ -24,50 +43,27 @@ const setupAssociationDataSourceModal = ({ const coreServices = coreMock.createStart(); jest.spyOn(utilsExports, 'getDataSourcesList').mockResolvedValue([]); - jest.spyOn(utilsExports, 'getOpenSearchAndDataConnections').mockResolvedValue({ - openSearchConnections: [ - { - id: 'ds1', - name: 'Data Source 1', - type: 'OpenSearch', - connectionType: 0, - relatedConnections: [], - }, - ], - dataConnections: [ - { - id: 'dqs1', - name: 'Data Connection 1', - connectionType: DataSourceConnectionType.DataConnection, - type: 'AWS Security Lake', - }, - ], - }); - jest - .spyOn(utilsExports, 'updateFullFillRelatedConnections') - .mockImplementationOnce( - ( - openSearchConnections: DataSourceConnection[], - directQueryConnections: DataSourceConnection[] - ): DataSourceConnection[] => [ + .spyOn(utilsExports, 'getOpenSearchAndDataConnections') + .mockReturnValue(openSearchAndDataConnectionsMock); + + jest.spyOn(utilsExports, 'updateFullFillRelatedConnections').mockReturnValue([ + { + id: 'ds1', + name: 'Data Source 1', + type: 'OpenSearch', + connectionType: DataSourceConnectionType.OpenSearchConnection, + relatedConnections: [ { - id: 'ds1', - name: 'Data Source 1', - type: 'OpenSearch', - connectionType: 0, - relatedConnections: [ - { - id: 'ds1-dqc1', - name: 'dqc1', - type: 'Amazon S3', - connectionType: 1, - parentId: 'ds1', - }, - ], + id: 'ds1-dqc1', + name: 'dqc1', + type: 'Amazon S3', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', }, - ] - ); + ], + }, + ]); jest.spyOn(utilsExports, 'fetchDirectQueryConnections').mockResolvedValue([ { 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 99bce7150642..47aa6c70d490 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 @@ -169,6 +169,7 @@ const convertConnectionsToOptions = ({ if (connection.connectionType === DataSourceConnectionType.OpenSearchConnection) { if (showDirectQueryConnections) { if (!connection.relatedConnections || connection.relatedConnections.length === 0) { + // return [connection] for the case where the connnection has no direct connections for now, but it may have in the future return [connection]; } return [ @@ -253,15 +254,13 @@ export const AssociationDataSourceModalContent = ({ useEffect(() => { setIsLoading(true); - setIsRelatedConnectionsLoaded(false); getDataSourcesList(savedObjects.client, ['*']) .then((dataSourcesList) => { - return getOpenSearchAndDataConnections(dataSourcesList, notifications).then( - ({ openSearchConnections, dataConnections }) => { - setAllConnections([...openSearchConnections, ...dataConnections]); - return { openSearchConnections, dataConnections, dataSourcesList }; - } + const { openSearchConnections, dataConnections } = getOpenSearchAndDataConnections( + dataSourcesList ); + setAllConnections([...openSearchConnections, ...dataConnections]); + return { openSearchConnections, dataConnections, dataSourcesList }; }) .then(({ openSearchConnections, dataConnections, dataSourcesList }) => { fetchDirectQueryConnections(dataSourcesList, http, notifications).then( 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..8facb6d7c951 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 @@ -16,7 +16,7 @@ import { WorkspaceCreatorProps, } from './workspace_creator'; import { DataSourceEngineType } from '../../../../data_source/common/data_sources'; -import { DataSourceConnectionType } from '../../../common/types'; +import { DataSourceConnectionType, DataSourceConnection } from '../../../common/types'; import * as utils from '../../utils'; import * as workspaceUtilsExports from '../utils/workspace'; @@ -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,76 @@ 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, 'getOpenSearchAndDataConnections').mockReturnValue({ + openSearchConnections: [...dataSourceConnectionsList], + dataConnections: [...dataConnectionsList], }); +jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSourcesList); +jest + .spyOn(utils, 'updateFullFillRelatedConnections') + .mockImplementationOnce( + ( + openSearchConnections: DataSourceConnection[], + directQueryConnections: DataSourceConnection[] + ): DataSourceConnection[] => [ + { + 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, 'fetchDirectQueryConnections').mockResolvedValue(directQueryConnectionsMock); + +const mockCoreStart = coreMock.createStart(); + const WorkspaceCreator = ({ isDashboardAdmin = false, dataSourceEnabled = false, @@ -382,7 +422,7 @@ describe('WorkspaceCreator', () => { }), expect.objectContaining({ dataConnections: [], - dataSources: ['id1'], + dataSources: ['ds1'], }) ); await waitFor(() => { @@ -431,7 +471,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 393018db1c83..af9afb465b73 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 @@ -58,11 +58,11 @@ const dataSources = [ }, ]; +jest + .spyOn(utils, 'getOpenSearchAndDataConnections') + .mockReturnValue({ openSearchConnections: [...dataSourceConnectionsMock], dataConnections: [] }); + jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSources); -jest.spyOn(utils, 'getOpenSearchAndDataConnections').mockResolvedValue({ - openSearchConnections: [...dataSourceConnectionsMock], - dataConnections: [], -}); jest .spyOn(utils, 'updateFullFillRelatedConnections') diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts index 9e717ba00c57..16cb860dde47 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, diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 666d58e224b7..c549e039c43f 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -569,28 +569,18 @@ export const fetchDirectQueryConnections = async ( } }; -export const getOpenSearchAndDataConnections = async ( - dataSources: DataSource[] | DataConnection[], - notifications: NotificationsStart | undefined -): Promise<{ +export const getOpenSearchAndDataConnections = ( + dataSources: DataSource[] | DataConnection[] +): { openSearchConnections: DataSourceConnection[]; dataConnections: DataSourceConnection[]; -}> => { - try { - const { - openSearchConnections, - dataConnections, - } = convertDataSourcesToOpenSearchAndDataConnections(dataSources); +} => { + const { + openSearchConnections, + dataConnections, + } = convertDataSourcesToOpenSearchAndDataConnections(dataSources); - return { openSearchConnections, dataConnections }; - } catch (error) { - notifications?.toasts.addDanger( - i18n.translate('workspace.detail.dataSources.connections.error.message', { - defaultMessage: 'Cannot fetch OpenSearch connections and data connections ', - }) - ); - return { openSearchConnections: [], dataConnections: [] }; - } + return { openSearchConnections, dataConnections }; }; export const getUseCase = (workspace: WorkspaceObject, availableUseCases: WorkspaceUseCase[]) => { From ee0299fd6eb516484b6740ee9a0a274d3975b57c Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Wed, 11 Dec 2024 10:47:10 +0800 Subject: [PATCH 5/7] delete functions Signed-off-by: Qxisylolo --- .../association_data_source_modal.test.tsx | 4 +- .../association_data_source_modal.tsx | 13 ++-- .../workspace_creator.test.tsx | 39 +++++------ .../select_data_source_panel.test.tsx | 65 ++++++++----------- src/plugins/workspace/public/utils.ts | 21 ------ 5 files changed, 51 insertions(+), 91 deletions(-) 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 785d640c3e82..fa29283e56bc 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 @@ -44,10 +44,10 @@ const setupAssociationDataSourceModal = ({ jest.spyOn(utilsExports, 'getDataSourcesList').mockResolvedValue([]); jest - .spyOn(utilsExports, 'getOpenSearchAndDataConnections') + .spyOn(utilsExports, 'convertDataSourcesToOpenSearchAndDataConnections') .mockReturnValue(openSearchAndDataConnectionsMock); - jest.spyOn(utilsExports, 'updateFullFillRelatedConnections').mockReturnValue([ + jest.spyOn(utilsExports, 'fulfillRelatedConnections').mockReturnValue([ { id: 'ds1', name: 'Data Source 1', 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 47aa6c70d490..732f9e388207 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 @@ -27,9 +27,9 @@ import { i18n } from '@osd/i18n'; import { getDataSourcesList, - getOpenSearchAndDataConnections, + fulfillRelatedConnections, fetchDirectQueryConnections, - updateFullFillRelatedConnections, + convertDataSourcesToOpenSearchAndDataConnections, } from '../../utils'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { HttpStart, NotificationsStart, SavedObjectsStart } from '../../../../../core/public'; @@ -256,16 +256,17 @@ export const AssociationDataSourceModalContent = ({ setIsLoading(true); getDataSourcesList(savedObjects.client, ['*']) .then((dataSourcesList) => { - const { openSearchConnections, dataConnections } = getOpenSearchAndDataConnections( - dataSourcesList - ); + const { + openSearchConnections, + dataConnections, + } = convertDataSourcesToOpenSearchAndDataConnections(dataSourcesList); setAllConnections([...openSearchConnections, ...dataConnections]); return { openSearchConnections, dataConnections, dataSourcesList }; }) .then(({ openSearchConnections, dataConnections, dataSourcesList }) => { fetchDirectQueryConnections(dataSourcesList, http, notifications).then( (directQueryConnections) => { - const updatedOpenSearchConnections = updateFullFillRelatedConnections( + const updatedOpenSearchConnections = fulfillRelatedConnections( openSearchConnections, directQueryConnections ); 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 8facb6d7c951..3e833dd0602e 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 @@ -116,36 +116,29 @@ const dataConnectionsList = [ }, ]; -jest.spyOn(utils, 'getOpenSearchAndDataConnections').mockReturnValue({ +jest.spyOn(utils, 'convertDataSourcesToOpenSearchAndDataConnections').mockReturnValue({ openSearchConnections: [...dataSourceConnectionsList], dataConnections: [...dataConnectionsList], }); jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSourcesList); -jest - .spyOn(utils, 'updateFullFillRelatedConnections') - .mockImplementationOnce( - ( - openSearchConnections: DataSourceConnection[], - directQueryConnections: DataSourceConnection[] - ): DataSourceConnection[] => [ +jest.spyOn(utils, 'fulfillRelatedConnections').mockReturnValue([ + { + id: 'ds1', + name: 'Data Source 1', + type: 'OpenSearch', + connectionType: 0, + relatedConnections: [ { - id: 'ds1', - name: 'Data Source 1', - type: 'OpenSearch', - connectionType: 0, - relatedConnections: [ - { - id: 'ds1-dqc1', - name: 'dqc1', - type: 'Amazon S3', - connectionType: 1, - parentId: 'ds1', - }, - ], + id: 'ds1-dqc1', + name: 'dqc1', + type: 'Amazon S3', + connectionType: 1, + parentId: 'ds1', }, - ] - ); + ], + }, +]); jest.spyOn(utils, 'fetchDirectQueryConnections').mockResolvedValue(directQueryConnectionsMock); 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 af9afb465b73..e66b6ba06bd2 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 @@ -10,7 +10,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import * as utils from '../../utils'; import { DataSourceEngineType } from 'src/plugins/data_source/common/data_sources'; import { OpenSearchDashboardsContextProvider } from '../../../../../plugins/opensearch_dashboards_react/public'; -import { DataSourceConnectionType, DataSourceConnection } from '../../../common/types'; +import { DataSourceConnectionType } from '../../../common/types'; import { SelectDataSourcePanel, SelectDataSourcePanelProps } from './select_data_source_panel'; @@ -57,39 +57,31 @@ const dataSources = [ workspaces: [], }, ]; +jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSources); jest - .spyOn(utils, 'getOpenSearchAndDataConnections') + .spyOn(utils, 'convertDataSourcesToOpenSearchAndDataConnections') .mockReturnValue({ openSearchConnections: [...dataSourceConnectionsMock], dataConnections: [] }); -jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSources); +jest.spyOn(utils, 'fetchDirectQueryConnections').mockResolvedValue(directQueryConnectionsMock); -jest - .spyOn(utils, 'updateFullFillRelatedConnections') - .mockImplementationOnce( - ( - openSearchConnections: DataSourceConnection[], - directQueryConnections: DataSourceConnection[] - ): DataSourceConnection[] => [ +jest.spyOn(utils, 'fulfillRelatedConnections').mockReturnValue([ + { + id: 'ds1', + name: 'Data Source 1', + type: 'OpenSearch', + connectionType: DataSourceConnectionType.OpenSearchConnection, + relatedConnections: [ { - id: 'ds1', - name: 'Data Source 1', - type: 'OpenSearch', - connectionType: 0, - relatedConnections: [ - { - id: 'ds1-dqc1', - name: 'dqc1', - type: 'Amazon S3', - connectionType: 1, - parentId: 'ds1', - }, - ], + id: 'ds1-dqc1', + name: 'dqc1', + type: 'Amazon S3', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', }, - ] - ); - -jest.spyOn(utils, 'fetchDirectQueryConnections').mockResolvedValue(directQueryConnectionsMock); + ], + }, +]); const mockCoreStart = coreMock.createStart(); @@ -163,24 +155,19 @@ describe('SelectDataSourcePanel', () => { 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('Data Source 1')).toBeInTheDocument(); - expect(getByText('Data Source 2')).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(); + await findByText('Data Source 1'); fireEvent.click(getByText('Data Source 1')); fireEvent.click(getByText('Associate data sources')); expect(onChangeMock).toHaveBeenCalledWith([expect.objectContaining({ id: 'ds1' })]); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index c549e039c43f..0f3a7dbbc006 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -539,13 +539,6 @@ export const fetchDataSourceConnections = async ( } }; -export const updateFullFillRelatedConnections = ( - openSearchConnections: DataSourceConnection[], - directQueryConnections: DataSourceConnection[] -): DataSourceConnection[] => { - return fulfillRelatedConnections(openSearchConnections, directQueryConnections); -}; - export const fetchDirectQueryConnections = async ( dataSources: DataSource[], http: HttpSetup | undefined, @@ -569,20 +562,6 @@ export const fetchDirectQueryConnections = async ( } }; -export const getOpenSearchAndDataConnections = ( - dataSources: DataSource[] | DataConnection[] -): { - openSearchConnections: DataSourceConnection[]; - dataConnections: DataSourceConnection[]; -} => { - const { - openSearchConnections, - dataConnections, - } = convertDataSourcesToOpenSearchAndDataConnections(dataSources); - - return { openSearchConnections, dataConnections }; -}; - export const getUseCase = (workspace: WorkspaceObject, availableUseCases: WorkspaceUseCase[]) => { if (!workspace.features) { return; From c506daa69ae12f7dc30f48428fa1f1f7a4e97585 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Wed, 11 Dec 2024 17:44:45 +0800 Subject: [PATCH 6/7] separately fetch dqc Signed-off-by: Qxisylolo --- .../association_data_source_modal.test.tsx | 66 ++++++++++------- .../association_data_source_modal.tsx | 73 +++++++++++-------- .../workspace_creator.test.tsx | 4 +- .../select_data_source_panel.test.tsx | 20 +---- src/plugins/workspace/public/utils.ts | 6 +- 5 files changed, 86 insertions(+), 83 deletions(-) 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 fa29283e56bc..72e1aae42d6e 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 @@ -2,7 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor, act } from '@testing-library/react'; import React from 'react'; import { IntlProvider } from 'react-intl'; @@ -15,6 +15,32 @@ 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 openSearchAndDataConnectionsMock = { openSearchConnections: [ @@ -41,37 +67,18 @@ const setupAssociationDataSourceModal = ({ handleAssignDataSourceConnections, }: Partial = {}) => { const coreServices = coreMock.createStart(); - jest.spyOn(utilsExports, 'getDataSourcesList').mockResolvedValue([]); + jest.spyOn(utilsExports, 'getDataSourcesList').mockResolvedValue(dataSourcesList); jest .spyOn(utilsExports, 'convertDataSourcesToOpenSearchAndDataConnections') .mockReturnValue(openSearchAndDataConnectionsMock); - jest.spyOn(utilsExports, 'fulfillRelatedConnections').mockReturnValue([ - { - id: 'ds1', - name: 'Data Source 1', - type: 'OpenSearch', - connectionType: DataSourceConnectionType.OpenSearchConnection, - relatedConnections: [ - { - id: 'ds1-dqc1', - name: 'dqc1', - type: 'Amazon S3', - connectionType: DataSourceConnectionType.DirectQueryConnection, - parentId: 'ds1', - }, - ], - }, - ]); - - jest.spyOn(utilsExports, 'fetchDirectQueryConnections').mockResolvedValue([ + jest.spyOn(utilsExports, 'fetchDirectQueryConnectionsByIDs').mockResolvedValue([ { id: 'ds1-dqc1', name: 'dqc1', type: 'Amazon S3', connectionType: 1, - description: 'direct_query_connections_1', parentId: 'ds1', }, ]); @@ -165,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', @@ -196,11 +205,12 @@ describe('AssociationDataSourceModal', () => { handleAssignDataSourceConnections: handleAssignDataSourceConnectionsMock, mode: AssociationDataSourceModalMode.DirectQueryConnections, }); - await waitFor(() => { - fireEvent.click(screen.getByRole('option', { name: 'Data Connection 1' })); - fireEvent.click(screen.getByRole('button', { name: 'Associate data sources' })); + 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([ { 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 732f9e388207..dc61fc0ef358 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 @@ -27,8 +27,7 @@ import { i18n } from '@osd/i18n'; import { getDataSourcesList, - fulfillRelatedConnections, - fetchDirectQueryConnections, + fetchDirectQueryConnectionsByIDs, convertDataSourcesToOpenSearchAndDataConnections, } from '../../utils'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; @@ -93,18 +92,18 @@ const convertConnectionToOption = ({ connection, selectedConnectionIds, logos, - isRelatedConnectionsLoaded, + loadingStatus, }: { connection: DataSourceConnection; selectedConnectionIds: string[]; logos: Logos; - isRelatedConnectionsLoaded: boolean; + loadingStatus: Record; }) => { return { label: connection.name, key: connection.id, description: connection.description, - append: isRelatedConnectionsLoaded ? ( + append: !loadingStatus[connection.id] ? ( connection.relatedConnections && connection.relatedConnections.length > 0 ? ( {i18n.translate('workspace.form.selectDataSource.optionBadge', { @@ -141,14 +140,14 @@ const convertConnectionsToOptions = ({ selectedConnectionIds, excludedConnectionIds, logos, - isRelatedConnectionsLoaded, + loadingStatus, }: { connections: DataSourceConnection[]; excludedConnectionIds: string[]; showDirectQueryConnections: boolean; selectedConnectionIds: string[]; logos: Logos; - isRelatedConnectionsLoaded: boolean; + loadingStatus: Record; }) => { return connections .flatMap((connection) => { @@ -186,7 +185,7 @@ const convertConnectionsToOptions = ({ connection, selectedConnectionIds, logos, - isRelatedConnectionsLoaded, + loadingStatus, }) ); }; @@ -222,11 +221,11 @@ export const AssociationDataSourceModalContent = ({ }: AssociationDataSourceModalProps) => { const [allConnections, setAllConnections] = useState([]); const [selectedConnectionIds, setSelectedConnectionIds] = useState([]); - const [isRelatedConnectionsLoaded, setIsRelatedConnectionsLoaded] = useState(false); const [options, setOptions] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const mountedRef = useRef(false); + const [loadingStatus, setLoadingStatus] = useState>({}); useEffect(() => { mountedRef.current = true; @@ -260,21 +259,40 @@ export const AssociationDataSourceModalContent = ({ openSearchConnections, dataConnections, } = convertDataSourcesToOpenSearchAndDataConnections(dataSourcesList); + + const initialLoadingStatus = dataSourcesList.reduce((acc, ds) => { + acc[ds.id] = true; + return acc; + }, {} as Record); + + setLoadingStatus(initialLoadingStatus); + + // display data sources connections first while loading direct query connection setAllConnections([...openSearchConnections, ...dataConnections]); - return { openSearchConnections, dataConnections, dataSourcesList }; + return { openSearchConnections }; }) - .then(({ openSearchConnections, dataConnections, dataSourcesList }) => { - fetchDirectQueryConnections(dataSourcesList, http, notifications).then( - (directQueryConnections) => { - const updatedOpenSearchConnections = fulfillRelatedConnections( - openSearchConnections, - directQueryConnections - ); - - setAllConnections([...updatedOpenSearchConnections, ...dataConnections]); - setIsRelatedConnectionsLoaded(true); // related connections are completely loaded - } - ); + .then(({ openSearchConnections }) => { + // Only data source saved object type needs to fetch data source connections, data connection type object not. + openSearchConnections.forEach((ds) => { + // fetch direct query connections for each data source, and set loading status accordingly + fetchDirectQueryConnectionsByIDs([ds.id], http, notifications) + .then((directQueryConnections) => { + setAllConnections((prev) => { + return prev.map((connection) => { + if (connection.id === ds.id) { + return { + ...connection, + relatedConnections: directQueryConnections, + }; + } + return connection; + }); + }); + }) + .finally(() => { + setLoadingStatus((prev) => ({ ...prev, [ds.id]: false })); + }); + }); }) .finally(() => { setIsLoading(false); @@ -291,18 +309,11 @@ export const AssociationDataSourceModalContent = ({ showDirectQueryConnections: mode === AssociationDataSourceModalMode.DirectQueryConnections, logos, - isRelatedConnectionsLoaded, + loadingStatus, }) ); } - }, [ - excludedConnectionIds, - selectedConnectionIds, - mode, - allConnections, - logos, - isRelatedConnectionsLoaded, - ]); + }, [excludedConnectionIds, selectedConnectionIds, mode, allConnections, logos, loadingStatus]); 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 3e833dd0602e..adaa0f184208 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 @@ -16,7 +16,7 @@ import { WorkspaceCreatorProps, } from './workspace_creator'; import { DataSourceEngineType } from '../../../../data_source/common/data_sources'; -import { DataSourceConnectionType, DataSourceConnection } from '../../../common/types'; +import { DataSourceConnectionType } from '../../../common/types'; import * as utils from '../../utils'; import * as workspaceUtilsExports from '../utils/workspace'; @@ -140,7 +140,7 @@ jest.spyOn(utils, 'fulfillRelatedConnections').mockReturnValue([ }, ]); -jest.spyOn(utils, 'fetchDirectQueryConnections').mockResolvedValue(directQueryConnectionsMock); +jest.spyOn(utils, 'fetchDirectQueryConnectionsByIDs').mockResolvedValue(directQueryConnectionsMock); const mockCoreStart = coreMock.createStart(); 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 e66b6ba06bd2..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 @@ -63,25 +63,7 @@ jest .spyOn(utils, 'convertDataSourcesToOpenSearchAndDataConnections') .mockReturnValue({ openSearchConnections: [...dataSourceConnectionsMock], dataConnections: [] }); -jest.spyOn(utils, 'fetchDirectQueryConnections').mockResolvedValue(directQueryConnectionsMock); - -jest.spyOn(utils, 'fulfillRelatedConnections').mockReturnValue([ - { - id: 'ds1', - name: 'Data Source 1', - type: 'OpenSearch', - connectionType: DataSourceConnectionType.OpenSearchConnection, - relatedConnections: [ - { - id: 'ds1-dqc1', - name: 'dqc1', - type: 'Amazon S3', - connectionType: DataSourceConnectionType.DirectQueryConnection, - parentId: 'ds1', - }, - ], - }, -]); +jest.spyOn(utils, 'fetchDirectQueryConnectionsByIDs').mockResolvedValue(directQueryConnectionsMock); const mockCoreStart = coreMock.createStart(); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 0f3a7dbbc006..c0225a571367 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -539,15 +539,15 @@ export const fetchDataSourceConnections = async ( } }; -export const fetchDirectQueryConnections = async ( - dataSources: DataSource[], +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. - dataSources.filter((ds) => ds.type === DATA_SOURCE_SAVED_OBJECT_TYPE).map((ds) => ds.id), + dataSourceIds, http ); From 9b4259d3ed1556ff2de27f292828e0660223a16c Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Wed, 11 Dec 2024 18:28:36 +0800 Subject: [PATCH 7/7] small mistakes Signed-off-by: Qxisylolo --- .../association_data_source_modal.test.tsx | 4 ++-- .../association_data_source_modal.tsx | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) 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 72e1aae42d6e..f3057716f771 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 @@ -2,7 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { fireEvent, render, screen, waitFor, act } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { IntlProvider } from 'react-intl'; @@ -78,7 +78,7 @@ const setupAssociationDataSourceModal = ({ id: 'ds1-dqc1', name: 'dqc1', type: 'Amazon S3', - connectionType: 1, + connectionType: DataSourceConnectionType.DirectQueryConnection, parentId: 'ds1', }, ]); 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 dc61fc0ef358..8feba8a52a44 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 @@ -259,9 +259,12 @@ export const AssociationDataSourceModalContent = ({ openSearchConnections, dataConnections, } = convertDataSourcesToOpenSearchAndDataConnections(dataSourcesList); - - const initialLoadingStatus = dataSourcesList.reduce((acc, ds) => { - acc[ds.id] = true; + // Only data source saved object type needs to fetch data source connections, data connection type object not, use loadingStatus to track the loading status of each data source connections + const initialLoadingStatus = [ + ...openSearchConnections.map((ds) => ({ id: ds.id, status: true })), + ...dataConnections.map((ds) => ({ id: ds.id, status: false })), + ].reduce((acc, { id, status }) => { + acc[id] = status; return acc; }, {} as Record); @@ -272,9 +275,8 @@ export const AssociationDataSourceModalContent = ({ return { openSearchConnections }; }) .then(({ openSearchConnections }) => { - // Only data source saved object type needs to fetch data source connections, data connection type object not. openSearchConnections.forEach((ds) => { - // fetch direct query connections for each data source, and set loading status accordingly + // only fetch direct query connections for data source saved object, and set loading status accordingly fetchDirectQueryConnectionsByIDs([ds.id], http, notifications) .then((directQueryConnections) => { setAllConnections((prev) => {