From 57db44dac010557aee864f840eb194b06c1b7863 Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Mon, 2 Sep 2024 13:59:00 +0800 Subject: [PATCH 01/21] support DQC Signed-off-by: Kapian1234 --- .../workspace_creator_form.tsx | 7 +- .../data_source_connection_table.tsx | 70 +++-- .../select_data_source_panel.test.tsx | 116 +++++--- .../select_data_source_panel.tsx | 256 ++++++++++-------- .../workspace_permission_setting_panel.tsx | 5 +- 5 files changed, 273 insertions(+), 181 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx index 9ab0a35e722..65f189b4f58 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx @@ -85,8 +85,8 @@ export const WorkspaceCreatorForm = (props: WorkspaceCreatorFormProps) => { ); return ( - - + + { errors={formErrors.selectedDataSources} onChange={setSelectedDataSources} savedObjects={savedObjects} - selectedDataSources={formData.selectedDataSources} + assignedDataSources={formData.selectedDataSources} data-test-subj={`workspaceForm-dataSourcePanel`} + isDashboardAdmin={true} /> diff --git a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx index 507ef32f07b..b990cf0013e 100644 --- a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx @@ -6,7 +6,6 @@ import './data_source_connection_table.scss'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { - EuiSpacer, EuiInMemoryTable, EuiBasicTableColumn, EuiTableSelectionType, @@ -32,7 +31,9 @@ interface DataSourceConnectionTableProps { isDashboardAdmin: boolean; connectionType: string; dataSourceConnections: DataSourceConnection[]; - handleUnassignDataSources: (dataSources: DataSourceConnection[]) => Promise; + handleUnassignDataSources: (dataSources: DataSourceConnection[]) => void; + onSelectedItems?: (dataSources: DataSourceConnection[]) => void; + inCreatePage?: boolean; } export const DataSourceConnectionTable = ({ @@ -40,6 +41,8 @@ export const DataSourceConnectionTable = ({ connectionType, dataSourceConnections, handleUnassignDataSources, + onSelectedItems, + inCreatePage = false, }: DataSourceConnectionTableProps) => { const [selectedItems, setSelectedItems] = useState([]); const [modalVisible, setModalVisible] = useState(false); @@ -47,6 +50,11 @@ export const DataSourceConnectionTable = ({ const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< Record >({}); + useEffect(() => { + if (onSelectedItems) { + onSelectedItems(selectedItems); + } + }, [selectedItems, onSelectedItems, inCreatePage]); useEffect(() => { // Reset selected items when connectionType changes @@ -174,7 +182,7 @@ export const DataSourceConnectionTable = ({ }, }, { - width: '10%', + width: '15%', field: 'type', name: i18n.translate('workspace.detail.dataSources.table.type', { defaultMessage: 'Type', @@ -182,7 +190,6 @@ export const DataSourceConnectionTable = ({ truncateText: true, }, { - width: '35%', field: 'description', name: i18n.translate('workspace.detail.dataSources.table.description', { defaultMessage: 'Description', @@ -190,11 +197,11 @@ export const DataSourceConnectionTable = ({ truncateText: true, }, { + width: '140px', field: 'relatedConnections', name: i18n.translate('workspace.detail.dataSources.table.relatedConnections', { defaultMessage: 'Related connections', }), - align: 'right', truncateText: true, render: (relatedConnections: DataSourceConnection[], record) => relatedConnections?.length > 0 ? ( @@ -267,12 +274,17 @@ export const DataSourceConnectionTable = ({ icon: 'unlink', type: 'icon', onClick: (item: DataSourceConnection) => { - setSelectedItems([item]); - setModalVisible(true); + if (inCreatePage) { + handleUnassignDataSources([item]); + } else { + setSelectedItems([item]); + setModalVisible(true); + } }, 'data-test-subj': 'workspace-detail-dataSources-table-actions-remove', }, ], + width: '10%', } as EuiTableActionsColumnType, ] : []), @@ -285,22 +297,34 @@ export const DataSourceConnectionTable = ({ return ( <> - - + {inCreatePage ? ( + + ) : ( + + )} {modalVisible && ( ({ - getDataSourcesList: jest.fn().mockResolvedValue(dataSources), -})); +const dataSources = [ + { + id: 'id3', + title: 'title3', + description: 'ds-3-description', + auth: '', + dataSourceEngineType: '' as DataSourceEngineType, + workspaces: [], + }, + { + id: 'id4', + title: 'title4', + description: 'ds-4-description', + auth: '', + dataSourceEngineType: '' as DataSourceEngineType, + workspaces: [], + }, +]; + +jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSources); +jest.spyOn(utils, 'fetchDataSourceConnections').mockResolvedValue(assignedDataSourcesConections); const mockCoreStart = coreMock.createStart(); const setup = ({ savedObjects = mockCoreStart.savedObjects, - selectedDataSources = [], + assignedDataSources = [], onChange = jest.fn(), errors = undefined, + isDashboardAdmin = true, }: Partial) => { return render( ); }; describe('SelectDataSourcePanel', () => { - it('should render consistent data sources when selected data sources passed', () => { - const { getByText } = setup({ selectedDataSources: dataSources }); + it('should render consistent data sources when selected data sources passed', async () => { + const { getByText } = setup({ assignedDataSources: dataSources }); - expect(getByText(dataSources[0].title)).toBeInTheDocument(); - expect(getByText(dataSources[1].title)).toBeInTheDocument(); + await waitFor(() => { + expect(getByText(assignedDataSourcesConections[0].name)).toBeInTheDocument(); + expect(getByText(assignedDataSourcesConections[1].name)).toBeInTheDocument(); + }); }); - it('should call onChange when clicking add new data source button', () => { + it('should call onChange when updating data sources', async () => { + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { + configurable: true, + value: 600, + }); + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { + configurable: true, + value: 600, + }); const onChangeMock = jest.fn(); - const { getByTestId } = setup({ onChange: onChangeMock }); + const { getByTestId, getAllByText, getByText } = setup({ + onChange: onChangeMock, + assignedDataSources: [], + }); expect(onChangeMock).not.toHaveBeenCalled(); - fireEvent.click(getByTestId('workspaceForm-select-dataSource-addNew')); + 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 connection, they will also be available in the workspace.' + ) + ).toBeInTheDocument(); + expect(getByText(dataSources[0].title)).toBeInTheDocument(); + }); + + fireEvent.click(getAllByText(dataSources[0].title)[0]); + fireEvent.click(getByText('Associate data sources')); expect(onChangeMock).toHaveBeenCalledWith([ { - id: '', - title: '', + connectionType: 0, + description: 'ds-3-description', + id: 'id3', + name: 'title3', + relatedConnections: [], + type: '', }, ]); }); - it('should call onChange when updating selected data sources in combo box', async () => { - const onChangeMock = jest.fn(); - const { getByTitle, getByText } = setup({ - onChange: onChangeMock, - selectedDataSources: [{ id: '', title: '' }], - }); - expect(onChangeMock).not.toHaveBeenCalled(); - await act(() => { - fireEvent.click(getByText('Select')); - }); - fireEvent.click(getByTitle(dataSources[0].title)); - expect(onChangeMock).toHaveBeenCalledWith([{ id: 'id1', title: 'title1' }]); - }); - it('should call onChange when deleting selected data source', async () => { const onChangeMock = jest.fn(); - const { getByLabelText } = setup({ + const { getAllByTestId } = setup({ onChange: onChangeMock, - selectedDataSources: [{ id: '', title: '' }], + assignedDataSources: dataSources, }); expect(onChangeMock).not.toHaveBeenCalled(); await act(() => { - fireEvent.click(getByLabelText('Delete data source')); + fireEvent.click(getAllByTestId('workspace-detail-dataSources-table-actions-remove')[0]); }); expect(onChangeMock).toHaveBeenCalledWith([]); }); diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index 9c990d7e719..3775697dcdd 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -3,149 +3,169 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; -import { - EuiSmallButton, - EuiCompressedFormRow, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, - EuiCompressedComboBox, - EuiComboBoxOptionOption, - EuiFormLabel, -} from '@elastic/eui'; +import React, { useState, useEffect } from 'react'; +import { EuiSpacer, EuiFlexItem, EuiSmallButton, EuiFlexGroup, EuiPanel } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import { SavedObjectsStart } from '../../../../../core/public'; -import { getDataSourcesList } from '../../utils'; -import { DataSource } from '../../../common/types'; +import { SavedObjectsStart, CoreStart, ChromeStart } from '../../../../../core/public'; +import { DataSource, DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { WorkspaceFormError } from './types'; +import { AssociationDataSourceModal } from '../workspace_detail/association_data_source_modal'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { WorkspaceClient } from '../../workspace_client'; +import { AssociationDataSourceModalMode } from '../../../common/constants'; +import { DataSourceConnectionTable } from '../workspace_detail/data_source_connection_table'; +import { fetchDataSourceConnections } from '../../utils'; +import { DataSourceEngineType } from '../../../../data_source/common/data_sources'; export interface SelectDataSourcePanelProps { errors?: { [key: number]: WorkspaceFormError }; savedObjects: SavedObjectsStart; - selectedDataSources: DataSource[]; + assignedDataSources: DataSource[]; onChange: (value: DataSource[]) => void; + isDashboardAdmin: boolean; } export const SelectDataSourcePanel = ({ errors, onChange, - selectedDataSources, + assignedDataSources, savedObjects, + isDashboardAdmin, }: SelectDataSourcePanelProps) => { - const [dataSourcesOptions, setDataSourcesOptions] = useState([]); + const [modalVisible, setModalVisible] = useState(false); + const [selectedItems, setSelectedItems] = useState([]); + const [assignedDataSourceConnections, setAssignedDataSourceConnections] = useState< + DataSourceConnection[] + >([]); + const [toggleIdSelected, setToggleIdSelected] = useState( + AssociationDataSourceModalMode.OpenSearchConnections + ); + const { + services: { notifications, http, chrome }, + } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); + useEffect(() => { - if (!savedObjects) return; - getDataSourcesList(savedObjects.client, ['*']).then((result) => { - const options = result.map(({ title, id }) => ({ - label: title, - value: id, - })); - setDataSourcesOptions(options); + fetchDataSourceConnections(assignedDataSources, http, notifications).then((connections) => { + setAssignedDataSourceConnections(connections); }); - }, [savedObjects, setDataSourcesOptions]); - const handleAddNewOne = useCallback(() => { - onChange?.([ - ...selectedDataSources, - { - title: '', - id: '', - }, - ]); - }, [onChange, selectedDataSources]); + }, [assignedDataSources, http, notifications]); + + const handleAssignDataSources = (dataSourceConnections: DataSourceConnection[]) => { + setModalVisible(false); + const dataSources = dataSourceConnections + .filter( + ({ connectionType }) => connectionType === DataSourceConnectionType.OpenSearchConnection + ) + .map(({ id, type, name, description }) => ({ + id, + title: name, + description, + dataSourceEngineType: type as DataSourceEngineType, + })); + const savedDataSources: DataSource[] = [...assignedDataSources, ...dataSources]; + onChange(savedDataSources); + }; - const handleSelect = useCallback( - (selectedOptions, index) => { - const newOption = selectedOptions[0] - ? // Select new data source - { - title: selectedOptions[0].label, - id: selectedOptions[0].value, - } - : // Click reset button - { - title: '', - id: '', - }; - const newSelectedOptions = [...selectedDataSources]; - newSelectedOptions.splice(index, 1, newOption); + const handleUnassignDataSources = (dataSourceConnections: DataSourceConnection[]) => { + const savedDataSources = (assignedDataSources ?? [])?.filter( + ({ id }: DataSource) => !dataSourceConnections.some((item) => item.id === id) + ); + onChange(savedDataSources); + }; - onChange(newSelectedOptions); - }, - [onChange, selectedDataSources] + const renderTableContent = () => { + return ( + + + + ); + }; + + const addOpenSearchConnectionsButton = ( + { + setToggleIdSelected(AssociationDataSourceModalMode.OpenSearchConnections); + setModalVisible(true); + }} + data-test-subj="workspace-creator-dataSources-assign-button" + > + {i18n.translate('workspace.form.selectDataSourcePanel.addNew', { + defaultMessage: 'Add data sources', + })} + ); - const handleDelete = useCallback( - (index) => { - const newSelectedOptions = [...selectedDataSources]; - newSelectedOptions.splice(index, 1); + const addDirectQueryConnectionsButton = ( + { + setToggleIdSelected(AssociationDataSourceModalMode.DirectQueryConnections); + setModalVisible(true); + }} + data-test-subj="workspace-creator-dqc-assign-button" + > + {i18n.translate('workspace.form.selectDataSourcePanel.addNew', { + defaultMessage: 'Add direct query connections', + })} + + ); - onChange(newSelectedOptions); - }, - [onChange, selectedDataSources] + const removeButton = ( + { + handleUnassignDataSources(selectedItems); + }} + data-test-subj="workspace-creator-dataSources-assign-button" + > + {i18n.translate('workspace.form.selectDataSourcePanel.remove', { + defaultMessage: 'Remove selected', + })} + ); + const getSelectedItems = (currentSelectedItems: DataSourceConnection[]) => + setSelectedItems(currentSelectedItems); + return (
- - {i18n.translate('workspace.form.selectDataSource.subTitle', { - defaultMessage: 'Data source', - })} - - - {selectedDataSources.map(({ id, title }, index) => ( - - - - handleSelect(selectedOptions, index)} - placeholder="Select" - /> - - - handleDelete(index)} - isDisabled={false} - /> - - - - ))} - - - {i18n.translate('workspace.form.selectDataSourcePanel.addNew', { - defaultMessage: 'Add New', - })} - + + + {isDashboardAdmin && selectedItems.length > 0 && assignedDataSources.length > 0 && ( + {removeButton} + )} + {isDashboardAdmin && ( + {addOpenSearchConnectionsButton} + )} + {isDashboardAdmin && ( + {addDirectQueryConnectionsButton} + )} + + + + {assignedDataSources.length > 0 && renderTableContent()} + + {modalVisible && chrome && ( + setModalVisible(false)} + handleAssignDataSourceConnections={handleAssignDataSources} + http={http} + mode={toggleIdSelected as AssociationDataSourceModalMode} + notifications={notifications} + logos={chrome?.logos} + /> + )}
); }; diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx index 8ea255b83b3..e3e630977c0 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx @@ -131,7 +131,7 @@ export const WorkspacePermissionSettingPanel = ({ return (
- + - + + {permissionSettings.map((item, index) => ( From 6111eb3587f795d88453db0f92894360dfc7eef5 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 2 Sep 2024 14:47:26 +0800 Subject: [PATCH 02/21] Fix UTs in workspace form select data source panel Signed-off-by: Lin Wang --- .../select_data_source_panel.test.tsx | 108 +++++++++++------- .../select_data_source_panel.tsx | 4 +- 2 files changed, 68 insertions(+), 44 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 603c563c8b4..4664f67bb87 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 @@ -4,42 +4,63 @@ */ import React from 'react'; -import { fireEvent, render, act, waitFor } from '@testing-library/react'; -import { SelectDataSourcePanel, SelectDataSourcePanelProps } from './select_data_source_panel'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { I18nProvider } from '@osd/i18n/react'; 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 { SelectDataSourcePanel, SelectDataSourcePanelProps } from './select_data_source_panel'; +import { IntlProvider } from 'react-intl'; -const assignedDataSourcesConections = [ +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', + }, + ], + }, { - id: 'id1', - name: 'title1', - description: 'ds-1-description', - type: 'OpenSearch' as DataSourceEngineType, - connectionType: 0, + id: 'ds1-dqc1', + name: 'dqc1', + parentId: 'ds1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + type: 'Amazon S3', }, { - id: 'id2', - name: 'title2', - description: 'ds-1-description', - type: 'S3GLUE' as DataSourceEngineType, - connectionType: 0, + id: 'ds2', + name: 'Data Source 2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', }, ]; +const assignedDataSourcesConnections = [dataSourceConnectionsMock[0], dataSourceConnectionsMock[1]]; + const dataSources = [ { - id: 'id3', - title: 'title3', - description: 'ds-3-description', + id: 'ds1', + title: 'Data Source 1', + description: 'Description of data source 1', auth: '', dataSourceEngineType: '' as DataSourceEngineType, workspaces: [], }, { - id: 'id4', - title: 'title4', - description: 'ds-4-description', + id: 'ds2', + title: 'Data Source 2', + description: 'Description of data source 2', auth: '', dataSourceEngineType: '' as DataSourceEngineType, workspaces: [], @@ -47,7 +68,11 @@ const dataSources = [ ]; jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSources); -jest.spyOn(utils, 'fetchDataSourceConnections').mockResolvedValue(assignedDataSourcesConections); +jest.spyOn(utils, 'fetchDataSourceConnections').mockImplementation(async (passedDataSources) => { + return dataSourceConnectionsMock.filter(({ id }) => + passedDataSources.some((dataSource) => dataSource.id === id) + ); +}); const mockCoreStart = coreMock.createStart(); @@ -59,13 +84,17 @@ const setup = ({ isDashboardAdmin = true, }: Partial) => { return render( - + + + + + ); }; @@ -74,8 +103,8 @@ describe('SelectDataSourcePanel', () => { const { getByText } = setup({ assignedDataSources: dataSources }); await waitFor(() => { - expect(getByText(assignedDataSourcesConections[0].name)).toBeInTheDocument(); - expect(getByText(assignedDataSourcesConections[1].name)).toBeInTheDocument(); + expect(getByText(dataSources[0].title)).toBeInTheDocument(); + expect(getByText(dataSources[1].title)).toBeInTheDocument(); }); }); @@ -89,7 +118,7 @@ describe('SelectDataSourcePanel', () => { value: 600, }); const onChangeMock = jest.fn(); - const { getByTestId, getAllByText, getByText } = setup({ + const { getByTestId, getByText } = setup({ onChange: onChangeMock, assignedDataSources: [], }); @@ -103,20 +132,15 @@ describe('SelectDataSourcePanel', () => { 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query connection, they will also be available in the workspace.' ) ).toBeInTheDocument(); - expect(getByText(dataSources[0].title)).toBeInTheDocument(); + expect(getByText(assignedDataSourcesConnections[0].name)).toBeInTheDocument(); }); - fireEvent.click(getAllByText(dataSources[0].title)[0]); + fireEvent.click(getByText(assignedDataSourcesConnections[0].name)); fireEvent.click(getByText('Associate data sources')); expect(onChangeMock).toHaveBeenCalledWith([ - { - connectionType: 0, - description: 'ds-3-description', - id: 'id3', - name: 'title3', - relatedConnections: [], - type: '', - }, + expect.objectContaining({ + id: assignedDataSourcesConnections[0].id, + }), ]); }); @@ -127,9 +151,9 @@ describe('SelectDataSourcePanel', () => { assignedDataSources: dataSources, }); expect(onChangeMock).not.toHaveBeenCalled(); - await act(() => { + await waitFor(() => { fireEvent.click(getAllByTestId('workspace-detail-dataSources-table-actions-remove')[0]); }); - expect(onChangeMock).toHaveBeenCalledWith([]); + expect(onChangeMock).toHaveBeenCalledWith([dataSources[1]]); }); }); diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index 3775697dcdd..371aeaa35b7 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -6,7 +6,7 @@ import React, { useState, useEffect } from 'react'; import { EuiSpacer, EuiFlexItem, EuiSmallButton, EuiFlexGroup, EuiPanel } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import { SavedObjectsStart, CoreStart, ChromeStart } from '../../../../../core/public'; +import { SavedObjectsStart, CoreStart } from '../../../../../core/public'; import { DataSource, DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { WorkspaceFormError } from './types'; import { AssociationDataSourceModal } from '../workspace_detail/association_data_source_modal'; @@ -125,7 +125,7 @@ export const SelectDataSourcePanel = ({ onClick={() => { handleUnassignDataSources(selectedItems); }} - data-test-subj="workspace-creator-dataSources-assign-button" + data-test-subj="workspace-creator-dataSources-remove-button" > {i18n.translate('workspace.form.selectDataSourcePanel.remove', { defaultMessage: 'Remove selected', From affc9baf8109253686012b6eed94e49a6148100f Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 2 Sep 2024 15:04:18 +0800 Subject: [PATCH 03/21] Remove no need IntlProvider Signed-off-by: Lin Wang --- .../components/workspace_form/select_data_source_panel.test.tsx | 1 - 1 file changed, 1 deletion(-) 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 4664f67bb87..4258f3ce8cf 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 @@ -13,7 +13,6 @@ import { OpenSearchDashboardsContextProvider } from '../../../../../plugins/open import { DataSourceConnectionType } from '../../../common/types'; import { SelectDataSourcePanel, SelectDataSourcePanelProps } from './select_data_source_panel'; -import { IntlProvider } from 'react-intl'; const dataSourceConnectionsMock = [ { From cff14910aa251a272688bc2c96e59b08b94dc0a9 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 2 Sep 2024 16:01:48 +0800 Subject: [PATCH 04/21] Add aria-labelledby for permission inputs Signed-off-by: Lin Wang --- .../components/workspace_form/constants.ts | 4 +++ .../workspace_permission_setting_input.tsx | 6 ++++ .../workspace_permission_setting_panel.tsx | 32 +++++++++---------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_form/constants.ts b/src/plugins/workspace/public/components/workspace_form/constants.ts index ed2268bec8d..c09ac11f548 100644 --- a/src/plugins/workspace/public/components/workspace_form/constants.ts +++ b/src/plugins/workspace/public/components/workspace_form/constants.ts @@ -123,3 +123,7 @@ export const DetailTabTitles: { [key in DetailTab]: string } = { defaultMessage: 'Collaborators', }), }; + +export const PERMISSION_TYPE_LABEL_ID = 'workspace-form-permission-type-label'; +export const PERMISSION_COLLABORATOR_LABEL_ID = 'workspace-form-permission-collaborator-label'; +export const PERMISSION_ACCESS_LEVEL_LABEL_ID = 'workspace-form-permission-access-level-label'; diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx index 00560f7c033..86f0d068871 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx @@ -18,6 +18,9 @@ import { WorkspacePermissionItemType, optionIdToWorkspacePermissionModesMap, PermissionModeId, + PERMISSION_TYPE_LABEL_ID, + PERMISSION_COLLABORATOR_LABEL_ID, + PERMISSION_ACCESS_LEVEL_LABEL_ID, } from './constants'; import { getPermissionModeId } from './utils'; @@ -148,6 +151,7 @@ export const WorkspacePermissionSettingInput = ({ onChange={(value) => onTypeChange(value, index)} disabled={userOrGroupDisabled || !isEditing} data-test-subj="workspace-typeOptions" + aria-labelledby={PERMISSION_TYPE_LABEL_ID} /> @@ -166,6 +170,7 @@ export const WorkspacePermissionSettingInput = ({ defaultMessage: 'Enter group name or group ID', }) } + aria-labelledby={PERMISSION_COLLABORATOR_LABEL_ID} /> @@ -176,6 +181,7 @@ export const WorkspacePermissionSettingInput = ({ onChange={handlePermissionModeOptionChange} disabled={userOrGroupDisabled || !isEditing} data-test-subj="workspace-permissionModeOptions" + aria-labelledby={PERMISSION_ACCESS_LEVEL_LABEL_ID} /> diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx index e3e630977c0..845708d7ecb 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx @@ -10,6 +10,7 @@ import { EuiFlexItem, EuiCompressedFormRow, EuiSpacer, + EuiFormLabel, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { WorkspaceFormError, WorkspacePermissionSetting } from './types'; @@ -17,6 +18,9 @@ import { WorkspacePermissionItemType, optionIdToWorkspacePermissionModesMap, PermissionModeId, + PERMISSION_TYPE_LABEL_ID, + PERMISSION_COLLABORATOR_LABEL_ID, + PERMISSION_ACCESS_LEVEL_LABEL_ID, } from './constants'; import { WorkspacePermissionSettingInput, @@ -130,34 +134,28 @@ export const WorkspacePermissionSettingPanel = ({ ); return ( -
+ <> - + {i18n.translate('workspaceForm.permissionSetting.typeLabel', { defaultMessage: 'Type', })} - > - <> - + - + {i18n.translate('workspaceForm.permissionSetting.collaboratorLabel', { defaultMessage: 'Collaborator', })} - > - <> - + - + {i18n.translate('workspaceForm.permissionSetting.accessLevelLabel', { defaultMessage: 'Access level', })} - > - <> - + @@ -196,6 +194,6 @@ export const WorkspacePermissionSettingPanel = ({ })} )} -
+ ); }; From 8490ca0dc265b5a1a702d6040dcfc19da8fb2f6d Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Mon, 2 Sep 2024 16:01:50 +0800 Subject: [PATCH 05/21] Modify UTs Signed-off-by: Kapian1234 --- .../workspace_creator.test.tsx | 59 +++++++++++++++++-- .../workspace_creator_form.tsx | 2 +- .../data_source_connection_table.tsx | 2 +- .../select_data_source_panel.tsx | 8 ++- 4 files changed, 60 insertions(+), 11 deletions(-) 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 9200ea7cfa0..af0018bce07 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 @@ -15,6 +15,9 @@ import { WorkspaceCreator as WorkspaceCreatorComponent, WorkspaceCreatorProps, } from './workspace_creator'; +import { DataSourceEngineType } from '../../../../data_source/common/data_sources'; +import { DataSourceConnectionType } from '../../../common/types'; +import * as utils from '../../utils'; const workspaceClientCreate = jest .fn() @@ -32,21 +35,50 @@ const dataSourcesList = [ { id: 'id1', title: 'ds1', + description: 'Description of data source 1', + auth: '', + dataSourceEngineType: '' as DataSourceEngineType, + workspaces: [], // This is used for mocking saved object function get: () => { return 'ds1'; }, }, { - id: '2', + id: 'id2', title: 'ds2', + description: 'Description of data source 1', + auth: '', + dataSourceEngineType: '' as DataSourceEngineType, + workspaces: [], get: () => { return 'ds2'; }, }, ]; +const dataSourceConnectionsList = [ + { + id: 'id1', + name: 'ds1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', + relatedConnections: [], + }, + { + id: 'id2', + name: 'ds2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', + }, +]; + const mockCoreStart = coreMock.createStart(); +jest.spyOn(utils, 'fetchDataSourceConnections').mockImplementation(async (passedDataSources) => { + return dataSourceConnectionsList.filter(({ id }) => + passedDataSources.some((dataSource) => dataSource.id === id) + ); +}); const WorkspaceCreator = ({ isDashboardAdmin = false, @@ -304,7 +336,15 @@ describe('WorkspaceCreator', () => { }); it('create workspace with customized selected dataSources', async () => { - const { getByTestId, getByTitle, getByText } = render( + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { + configurable: true, + value: 600, + }); + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { + configurable: true, + value: 600, + }); + const { getByTestId, getAllByText, getByText } = render( ); @@ -317,10 +357,17 @@ describe('WorkspaceCreator', () => { target: { value: 'test workspace name' }, }); fireEvent.click(getByTestId('workspaceUseCase-observability')); - fireEvent.click(getByTestId('workspaceForm-select-dataSource-addNew')); - fireEvent.click(getByTestId('workspaceForm-select-dataSource-comboBox')); - fireEvent.click(getByText('Select')); - fireEvent.click(getByTitle(dataSourcesList[0].title)); + 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 connection, they will also be available in the workspace.' + ) + ).toBeInTheDocument(); + expect(getByText(dataSourcesList[0].title)).toBeInTheDocument(); + }); + fireEvent.click(getByText(dataSourcesList[0].title)); + fireEvent.click(getAllByText('Associate data sources')[1]); fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); expect(workspaceClientCreate).toHaveBeenCalledWith( diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx index 65f189b4f58..bd60763564b 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx @@ -85,7 +85,7 @@ export const WorkspaceCreatorForm = (props: WorkspaceCreatorFormProps) => { ); return ( - + { // Reset selected items when connectionType changes diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index 371aeaa35b7..edb4b16bc00 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { EuiSpacer, EuiFlexItem, EuiSmallButton, EuiFlexGroup, EuiPanel } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { SavedObjectsStart, CoreStart } from '../../../../../core/public'; @@ -133,8 +133,10 @@ export const SelectDataSourcePanel = ({ ); - const getSelectedItems = (currentSelectedItems: DataSourceConnection[]) => - setSelectedItems(currentSelectedItems); + const getSelectedItems = useCallback( + (currentSelectedItems: DataSourceConnection[]) => setSelectedItems(currentSelectedItems), + [setSelectedItems] + ); return (
From b7916d0634934bc3f42fc2924c7ab23a492d93df Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 08:19:36 +0000 Subject: [PATCH 06/21] Changeset file for PR #7961 created/updated --- changelogs/fragments/7961.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/7961.yml diff --git a/changelogs/fragments/7961.yml b/changelogs/fragments/7961.yml new file mode 100644 index 00000000000..bdc020962e5 --- /dev/null +++ b/changelogs/fragments/7961.yml @@ -0,0 +1,2 @@ +feat: +- Support DQCs in create page ([#7961](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7961)) \ No newline at end of file From a362c4dcc24f867fb48e3be26fd12520036bccd5 Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Mon, 2 Sep 2024 18:20:34 +0800 Subject: [PATCH 07/21] Modify UTs Signed-off-by: Kapian1234 --- .../select_data_source_panel.test.tsx | 53 +++++++++++++++++-- .../select_data_source_panel.tsx | 4 +- 2 files changed, 50 insertions(+), 7 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 4258f3ce8cf..6d76548e24a 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 @@ -45,7 +45,7 @@ const dataSourceConnectionsMock = [ }, ]; -const assignedDataSourcesConnections = [dataSourceConnectionsMock[0], dataSourceConnectionsMock[1]]; +const assignedDataSourcesConnections = [dataSourceConnectionsMock[0], dataSourceConnectionsMock[2]]; const dataSources = [ { @@ -131,9 +131,21 @@ describe('SelectDataSourcePanel', () => { 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query connection, they will also be available in the workspace.' ) ).toBeInTheDocument(); - expect(getByText(assignedDataSourcesConnections[0].name)).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)); fireEvent.click(getByText('Associate data sources')); expect(onChangeMock).toHaveBeenCalledWith([ @@ -145,14 +157,45 @@ describe('SelectDataSourcePanel', () => { it('should call onChange when deleting selected data source', async () => { const onChangeMock = jest.fn(); - const { getAllByTestId } = setup({ + const { getByText, getByTestId } = setup({ onChange: onChangeMock, assignedDataSources: dataSources, }); expect(onChangeMock).not.toHaveBeenCalled(); await waitFor(() => { - fireEvent.click(getAllByTestId('workspace-detail-dataSources-table-actions-remove')[0]); + fireEvent.click(getByTestId('checkboxSelectRow-' + dataSources[1].id)); + fireEvent.click(getByText('Remove selected')); }); - expect(onChangeMock).toHaveBeenCalledWith([dataSources[1]]); + expect(onChangeMock).toHaveBeenCalledWith([dataSources[0]]); + }); +}); + +it('should close associate data sources modal', async () => { + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { + configurable: true, + value: 600, + }); + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { + configurable: true, + value: 600, + }); + + const { getByText, queryByText, getByTestId } = setup({ + assignedDataSources: [], + }); + + 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 connection, they will also be available in the workspace.' + ) + ).toBeInTheDocument(); }); + fireEvent.click(getByText('Close')); + expect( + queryByText( + 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query connection, they will also be available in the workspace.' + ) + ).toBeNull(); }); diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index edb4b16bc00..42acf39f544 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -112,7 +112,7 @@ export const SelectDataSourcePanel = ({ }} data-test-subj="workspace-creator-dqc-assign-button" > - {i18n.translate('workspace.form.selectDataSourcePanel.addNew', { + {i18n.translate('workspace.form.selectDataSourcePanel.addNewDQCs', { defaultMessage: 'Add direct query connections', })} @@ -153,7 +153,7 @@ export const SelectDataSourcePanel = ({ )} - + {assignedDataSources.length > 0 && renderTableContent()} {modalVisible && chrome && ( From 18b42947871f80e06f878730e7d0d3164a0cb56d Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Tue, 3 Sep 2024 10:37:05 +0800 Subject: [PATCH 08/21] Resolve some issues Signed-off-by: Kapian1234 --- .../data_source_connection_table.tsx | 14 ++++++++++---- .../workspace_form/select_data_source_panel.tsx | 14 ++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx index 9c56266c2a7..5595be8b3a6 100644 --- a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx @@ -26,6 +26,8 @@ import { i18n } from '@osd/i18n'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { AssociationDataSourceModalMode } from '../../../common/constants'; import { DirectQueryConnectionIcon } from '../workspace_form'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { CoreStart } from '../../../../../core/public'; interface DataSourceConnectionTableProps { isDashboardAdmin: boolean; @@ -50,6 +52,9 @@ export const DataSourceConnectionTable = ({ const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< Record >({}); + const { + services: { http }, + } = useOpenSearchDashboards(); useEffect(() => { if (onSelectedItems) { onSelectedItems(selectedItems); @@ -160,7 +165,7 @@ export const DataSourceConnectionTable = ({ ] : []), { - width: '25%', + width: '20%', field: 'name', name: i18n.translate('workspace.detail.dataSources.table.title', { defaultMessage: 'Title', @@ -168,11 +173,12 @@ export const DataSourceConnectionTable = ({ truncateText: true, render: (name: string, record) => { const origin = window.location.origin; + const basePath = http.basePath.serverBasePath; let url: string; if (record.connectionType === DataSourceConnectionType.OpenSearchConnection) { - url = `${origin}/app/dataSources/${record.id}`; + url = `${origin}${basePath}/app/dataSources/${record.id}`; } else { - url = `${origin}/app/dataSources/manage/${name}?dataSourceMDSId=${record.parentId}`; + url = `${origin}${basePath}/app/dataSources/manage/${name}?dataSourceMDSId=${record.parentId}`; } return ( @@ -182,7 +188,7 @@ export const DataSourceConnectionTable = ({ }, }, { - width: '15%', + width: '20%', field: 'type', name: i18n.translate('workspace.detail.dataSources.table.type', { defaultMessage: 'Type', diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index 42acf39f544..d65f2067f8c 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useCallback } from 'react'; import { EuiSpacer, EuiFlexItem, EuiSmallButton, EuiFlexGroup, EuiPanel } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { SavedObjectsStart, CoreStart } from '../../../../../core/public'; @@ -14,7 +14,6 @@ import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react import { WorkspaceClient } from '../../workspace_client'; import { AssociationDataSourceModalMode } from '../../../common/constants'; import { DataSourceConnectionTable } from '../workspace_detail/data_source_connection_table'; -import { fetchDataSourceConnections } from '../../utils'; import { DataSourceEngineType } from '../../../../data_source/common/data_sources'; export interface SelectDataSourcePanelProps { @@ -44,13 +43,8 @@ export const SelectDataSourcePanel = ({ services: { notifications, http, chrome }, } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); - useEffect(() => { - fetchDataSourceConnections(assignedDataSources, http, notifications).then((connections) => { - setAssignedDataSourceConnections(connections); - }); - }, [assignedDataSources, http, notifications]); - const handleAssignDataSources = (dataSourceConnections: DataSourceConnection[]) => { + setAssignedDataSourceConnections([...assignedDataSourceConnections, ...dataSourceConnections]); setModalVisible(false); const dataSources = dataSourceConnections .filter( @@ -67,6 +61,10 @@ export const SelectDataSourcePanel = ({ }; const handleUnassignDataSources = (dataSourceConnections: DataSourceConnection[]) => { + const savedDataSourcesConnctions = (assignedDataSourceConnections ?? [])?.filter( + ({ id }: DataSourceConnection) => !dataSourceConnections.some((item) => item.id === id) + ); + setAssignedDataSourceConnections(savedDataSourcesConnctions); const savedDataSources = (assignedDataSources ?? [])?.filter( ({ id }: DataSource) => !dataSourceConnections.some((item) => item.id === id) ); From 00875c0c75359e584d3053a4929fffe2c957c694 Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Tue, 3 Sep 2024 11:29:25 +0800 Subject: [PATCH 09/21] Modify UTs Signed-off-by: Kapian1234 --- .../data_source_connection_table.test.tsx | 16 +++++ .../select_data_source_panel.test.tsx | 60 ++++++++++++++++--- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.test.tsx b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.test.tsx index 06024c7b66c..d9585beb5b1 100644 --- a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.test.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.test.tsx @@ -8,7 +8,11 @@ import { DataSourceConnectionType } from '../../../common/types'; import React from 'react'; import { DataSourceConnectionTable } from './data_source_connection_table'; import { AssociationDataSourceModalMode } from '../../../common/constants'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +jest.mock('../../../../opensearch_dashboards_react/public', () => ({ + useOpenSearchDashboards: jest.fn(), +})); const handleUnassignDataSources = jest.fn(); const dataSourceConnectionsMock = [ { @@ -58,6 +62,18 @@ const dataSourceConnectionsMock = [ ]; describe('DataSourceConnectionTable', () => { + beforeEach(() => { + const mockHttp = { + basePath: { + serverBasePath: '', + }, + }; + (useOpenSearchDashboards as jest.Mock).mockImplementation(() => ({ + services: { + http: mockHttp, + }, + })); + }); afterEach(() => { jest.clearAllMocks(); }); 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 6d76548e24a..1a3f5e45608 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 @@ -98,8 +98,45 @@ const setup = ({ }; describe('SelectDataSourcePanel', () => { + const originalOffsetHeight = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + 'offsetHeight' + ); + const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth'); + beforeEach(() => { + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { + configurable: true, + value: 600, + }); + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { + configurable: true, + value: 600, + }); + }); + afterEach(() => { + Object.defineProperty( + HTMLElement.prototype, + 'offsetHeight', + originalOffsetHeight as PropertyDescriptor + ); + Object.defineProperty( + HTMLElement.prototype, + 'offsetWidth', + originalOffsetWidth as PropertyDescriptor + ); + }); it('should render consistent data sources when selected data sources passed', async () => { - const { getByText } = setup({ assignedDataSources: dataSources }); + const { getByText, getByTestId } = setup({ assignedDataSources: dataSources }); + fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); + + await waitFor(() => { + expect(getByText(assignedDataSourcesConnections[0].name)).toBeInTheDocument(); + expect(getByText(assignedDataSourcesConnections[1].name)).toBeInTheDocument(); + }); + + fireEvent.click(getByText(assignedDataSourcesConnections[0].name)); + fireEvent.click(getByText(assignedDataSourcesConnections[1].name)); + fireEvent.click(getByText('Associate data sources')); await waitFor(() => { expect(getByText(dataSources[0].title)).toBeInTheDocument(); @@ -108,14 +145,6 @@ describe('SelectDataSourcePanel', () => { }); it('should call onChange when updating data sources', async () => { - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { - configurable: true, - value: 600, - }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { - configurable: true, - value: 600, - }); const onChangeMock = jest.fn(); const { getByTestId, getByText } = setup({ onChange: onChangeMock, @@ -161,7 +190,20 @@ describe('SelectDataSourcePanel', () => { onChange: onChangeMock, assignedDataSources: dataSources, }); + fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); + + await waitFor(() => { + expect(getByText(assignedDataSourcesConnections[0].name)).toBeInTheDocument(); + expect(getByText(assignedDataSourcesConnections[1].name)).toBeInTheDocument(); + }); + + fireEvent.click(getByText(assignedDataSourcesConnections[0].name)); + fireEvent.click(getByText(assignedDataSourcesConnections[1].name)); + expect(onChangeMock).not.toHaveBeenCalled(); + + fireEvent.click(getByText('Associate data sources')); + await waitFor(() => { fireEvent.click(getByTestId('checkboxSelectRow-' + dataSources[1].id)); fireEvent.click(getByText('Remove selected')); From d3bd99f0656d9cb5e843a32959da997d8e7ca1c5 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 3 Sep 2024 13:59:41 +0800 Subject: [PATCH 10/21] Fix UT errror Signed-off-by: Lin Wang --- .../workspace_detail/data_source_connection_table.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.test.tsx b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.test.tsx index d9585beb5b1..17803ad29af 100644 --- a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.test.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.test.tsx @@ -11,6 +11,7 @@ import { AssociationDataSourceModalMode } from '../../../common/constants'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; jest.mock('../../../../opensearch_dashboards_react/public', () => ({ + ...jest.requireActual('../../../../opensearch_dashboards_react/public'), useOpenSearchDashboards: jest.fn(), })); const handleUnassignDataSources = jest.fn(); From c816ecd5fc3c234c04a17ba57c70cd077a503aec Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Tue, 3 Sep 2024 14:22:14 +0800 Subject: [PATCH 11/21] update button text Signed-off-by: Kapian1234 --- .../components/workspace_form/select_data_source_panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index d65f2067f8c..33613ce9b36 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -96,7 +96,7 @@ export const SelectDataSourcePanel = ({ data-test-subj="workspace-creator-dataSources-assign-button" > {i18n.translate('workspace.form.selectDataSourcePanel.addNew', { - defaultMessage: 'Add data sources', + defaultMessage: 'Add OpenSearch connections', })} ); From 65eed8b483ef39eee5ee1de25b0d1f0c7737507e Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Wed, 4 Sep 2024 10:17:37 +0800 Subject: [PATCH 12/21] rename onSelectItems() Signed-off-by: Kapian1234 --- .../workspace_detail/data_source_connection_table.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx index 5595be8b3a6..13be7d0eb6a 100644 --- a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx @@ -34,7 +34,7 @@ interface DataSourceConnectionTableProps { connectionType: string; dataSourceConnections: DataSourceConnection[]; handleUnassignDataSources: (dataSources: DataSourceConnection[]) => void; - onSelectedItems?: (dataSources: DataSourceConnection[]) => void; + onSelectItems?: (dataSources: DataSourceConnection[]) => void; inCreatePage?: boolean; } @@ -43,7 +43,7 @@ export const DataSourceConnectionTable = ({ connectionType, dataSourceConnections, handleUnassignDataSources, - onSelectedItems, + onSelectItems, inCreatePage = false, }: DataSourceConnectionTableProps) => { const [selectedItems, setSelectedItems] = useState([]); @@ -56,10 +56,10 @@ export const DataSourceConnectionTable = ({ services: { http }, } = useOpenSearchDashboards(); useEffect(() => { - if (onSelectedItems) { - onSelectedItems(selectedItems); + if (onSelectItems) { + onSelectItems(selectedItems); } - }, [selectedItems, onSelectedItems]); + }, [selectedItems, onSelectItems]); useEffect(() => { // Reset selected items when connectionType changes From 53d0fc3333c9f81dbbf7606743303f6882c0afc4 Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Wed, 4 Sep 2024 11:52:08 +0800 Subject: [PATCH 13/21] Fix an error Signed-off-by: Kapian1234 --- .../components/workspace_form/select_data_source_panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index 33613ce9b36..f9cef23b5a7 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -78,7 +78,7 @@ export const SelectDataSourcePanel = ({ isDashboardAdmin={isDashboardAdmin} dataSourceConnections={assignedDataSourceConnections} handleUnassignDataSources={handleUnassignDataSources} - onSelectedItems={getSelectedItems} + onSelectItems={getSelectedItems} inCreatePage={true} connectionType={AssociationDataSourceModalMode.OpenSearchConnections} /> From 2b4362a34390c645bee2a8097e25af093b32077c Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 4 Sep 2024 16:04:40 +0800 Subject: [PATCH 14/21] Refactor data source connection table Signed-off-by: Lin Wang --- .../select_data_source_panel.tsx | 4 +- ...orkspace_detail_connection_table.test.tsx} | 16 +- .../workspace_detail_connection_table.tsx | 125 +++++++++++++ .../data_source_connection_table.scss | 0 .../data_source_connection_table.tsx | 172 ++++-------------- .../public/components/workspace_form/index.ts | 1 + .../select_data_source_panel.tsx | 24 ++- 7 files changed, 179 insertions(+), 163 deletions(-) rename src/plugins/workspace/public/components/workspace_detail/{data_source_connection_table.test.tsx => workspace_detail_connection_table.test.tsx} (95%) create mode 100644 src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.tsx rename src/plugins/workspace/public/components/{workspace_detail => workspace_form}/data_source_connection_table.scss (100%) rename src/plugins/workspace/public/components/{workspace_detail => workspace_form}/data_source_connection_table.tsx (61%) diff --git a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx index 1f13da230f9..17abe39fdda 100644 --- a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx @@ -23,7 +23,7 @@ import { i18n } from '@osd/i18n'; import { FormattedMessage } from 'react-intl'; import { DataSource, DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { WorkspaceClient } from '../../workspace_client'; -import { DataSourceConnectionTable } from './data_source_connection_table'; +import { WorkspaceDetailConnectionTable } from './workspace_detail_connection_table'; import { AssociationDataSourceModal } from './association_data_source_modal'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { @@ -244,7 +244,7 @@ export const SelectDataSourceDetailPanel = ({ return noAssociationMessage; } return ( - ({ ...jest.requireActual('../../../../opensearch_dashboards_react/public'), @@ -62,7 +62,7 @@ const dataSourceConnectionsMock = [ }, ]; -describe('DataSourceConnectionTable', () => { +describe('WorkspaceDetailConnectionTable', () => { beforeEach(() => { const mockHttp = { basePath: { @@ -81,7 +81,7 @@ describe('DataSourceConnectionTable', () => { describe('OpenSearch connections', () => { it('renders the table with OpenSearch connections', () => { const { getByText, queryByText } = render( - { it('should show dqc popover when click the Related connections number ', () => { const { getByText } = render( - { it('should remove selected OpenSearch connections by dashboard admin', () => { const { getByText, queryByTestId, getAllByRole, getByRole } = render( - { it('should remove single OpenSearch connections by dashboard admin', () => { const { queryAllByTestId, getByText, getByRole } = render( - { it('should hide remove action iif user is not dashboard admin', () => { const { queryByText, queryByTestId, getAllByRole } = render( - { describe('Direct query connections', () => { it('renders the table with Direct query connections', () => { const { getByText, queryByText, getByTestId } = render( - void; +} + +export const WorkspaceDetailConnectionTable = ({ + isDashboardAdmin, + connectionType, + dataSourceConnections, + handleUnassignDataSources, +}: DataSourceConnectionTableProps) => { + const [selectedItems, setSelectedItems] = useState([]); + const [modalVisible, setModalVisible] = useState(false); + + useEffect(() => { + // Reset selected items when connectionType changes + setSelectedItems([]); + }, [connectionType]); + + const openSearchConnections = useMemo(() => { + return dataSourceConnections.filter((dsc) => + connectionType === AssociationDataSourceModalMode.OpenSearchConnections + ? dsc.connectionType === DataSourceConnectionType.OpenSearchConnection + : dsc?.relatedConnections && dsc.relatedConnections?.length > 0 + ); + }, [connectionType, dataSourceConnections]); + + const renderToolsLeft = useCallback(() => { + return selectedItems.length > 0 && !modalVisible + ? [ + setModalVisible(true)} + data-test-subj="workspace-detail-dataSources-table-bulkRemove" + > + {i18n.translate('workspace.detail.dataSources.table.remove.button', { + defaultMessage: 'Remove {numberOfSelect} association(s)', + values: { numberOfSelect: selectedItems.length }, + })} + , + ] + : []; + }, [selectedItems, modalVisible]); + + const search: EuiSearchBarProps = { + toolsLeft: renderToolsLeft(), + box: { + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'type', + name: 'Type', + multiSelect: 'or', + options: Array.from( + new Set(openSearchConnections.map(({ type }) => type).filter(Boolean)) + ).map((type) => ({ + value: type!, + name: type!, + })), + }, + ], + }; + + return ( + <> + { + { + setSelectedItems([item]); + setModalVisible(true); + }} + onSelectionChange={setSelectedItems} + /> + } + {modalVisible && ( + { + setModalVisible(false); + setSelectedItems([]); + }} + onConfirm={() => { + setModalVisible(false); + handleUnassignDataSources(selectedItems); + }} + cancelButtonText={i18n.translate('workspace.detail.dataSources.modal.cancelButton', { + defaultMessage: 'Cancel', + })} + confirmButtonText={i18n.translate('workspace.detail.dataSources.Modal.confirmButton', { + defaultMessage: 'Remove data source(s)', + })} + buttonColor="danger" + defaultFocusedButton="confirm" + /> + )} + + ); +}; diff --git a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.scss b/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.scss similarity index 100% rename from src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.scss rename to src/plugins/workspace/public/components/workspace_form/data_source_connection_table.scss diff --git a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx b/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx similarity index 61% rename from src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx rename to src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx index 13be7d0eb6a..0907324942b 100644 --- a/src/plugins/workspace/public/components/workspace_detail/data_source_connection_table.tsx +++ b/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx @@ -3,51 +3,54 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './data_source_connection_table.scss'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { EuiInMemoryTable, EuiBasicTableColumn, - EuiTableSelectionType, EuiTableActionsColumnType, - EuiConfirmModal, - EuiSearchBarProps, EuiText, EuiListGroup, EuiListGroupItem, EuiPopover, EuiButtonEmpty, EuiPopoverTitle, - EuiSmallButton, EuiLink, EuiButtonIcon, + EuiInMemoryTableProps, + EuiTableSelectionType, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { AssociationDataSourceModalMode } from '../../../common/constants'; -import { DirectQueryConnectionIcon } from '../workspace_form'; +import { DirectQueryConnectionIcon } from './direct_query_connection_icon'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { CoreStart } from '../../../../../core/public'; -interface DataSourceConnectionTableProps { +import './data_source_connection_table.scss'; + +type DataSourceConnectionTableProps = Omit< + EuiInMemoryTableProps, + | 'columns' + | 'itemsId' + | 'isSelectable' + | 'itemIdToExpandedRowMap' + | 'isExpandable' + | 'selection' + | 'pagination' +> & { isDashboardAdmin: boolean; connectionType: string; - dataSourceConnections: DataSourceConnection[]; - handleUnassignDataSources: (dataSources: DataSourceConnection[]) => void; - onSelectItems?: (dataSources: DataSourceConnection[]) => void; - inCreatePage?: boolean; -} + onUnlinkDataSource: (dataSources: DataSourceConnection) => void; + onSelectionChange: (selections: DataSourceConnection[]) => void; +}; export const DataSourceConnectionTable = ({ isDashboardAdmin, connectionType, - dataSourceConnections, - handleUnassignDataSources, - onSelectItems, - inCreatePage = false, + onUnlinkDataSource, + onSelectionChange, + ...restProps }: DataSourceConnectionTableProps) => { - const [selectedItems, setSelectedItems] = useState([]); - const [modalVisible, setModalVisible] = useState(false); const [popoversState, setPopoversState] = useState>({}); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< Record @@ -55,67 +58,6 @@ export const DataSourceConnectionTable = ({ const { services: { http }, } = useOpenSearchDashboards(); - useEffect(() => { - if (onSelectItems) { - onSelectItems(selectedItems); - } - }, [selectedItems, onSelectItems]); - - useEffect(() => { - // Reset selected items when connectionType changes - setSelectedItems([]); - setItemIdToExpandedRowMap({}); - }, [connectionType]); - - const openSearchConnections = useMemo(() => { - return dataSourceConnections.filter((dsc) => - connectionType === AssociationDataSourceModalMode.OpenSearchConnections - ? dsc.connectionType === DataSourceConnectionType.OpenSearchConnection - : dsc?.relatedConnections && dsc.relatedConnections?.length > 0 - ); - }, [connectionType, dataSourceConnections]); - - const renderToolsLeft = useCallback(() => { - return selectedItems.length > 0 && !modalVisible - ? [ - setModalVisible(true)} - data-test-subj="workspace-detail-dataSources-table-bulkRemove" - > - {i18n.translate('workspace.detail.dataSources.table.remove.button', { - defaultMessage: 'Remove {numberOfSelect} association(s)', - values: { numberOfSelect: selectedItems.length }, - })} - , - ] - : []; - }, [selectedItems, modalVisible]); - - const onSelectionChange = (selectedDataSources: DataSourceConnection[]) => { - setSelectedItems(selectedDataSources); - }; - - const search: EuiSearchBarProps = { - toolsLeft: renderToolsLeft(), - box: { - incremental: true, - }, - filters: [ - { - type: 'field_value_selection', - field: 'type', - name: 'Type', - multiSelect: 'or', - options: Array.from( - new Set(openSearchConnections.map(({ type }) => type).filter(Boolean)) - ).map((type) => ({ - value: type!, - name: type!, - })), - }, - ], - }; const togglePopover = (itemId: string) => { setPopoversState((prevState) => ({ @@ -280,12 +222,7 @@ export const DataSourceConnectionTable = ({ icon: 'unlink', type: 'icon', onClick: (item: DataSourceConnection) => { - if (inCreatePage) { - handleUnassignDataSources([item]); - } else { - setSelectedItems([item]); - setModalVisible(true); - } + onUnlinkDataSource(item); }, 'data-test-subj': 'workspace-detail-dataSources-table-actions-remove', }, @@ -302,59 +239,14 @@ export const DataSourceConnectionTable = ({ }; return ( - <> - {inCreatePage ? ( - - ) : ( - - )} - {modalVisible && ( - { - setModalVisible(false); - setSelectedItems([]); - }} - onConfirm={() => { - setModalVisible(false); - handleUnassignDataSources(selectedItems); - }} - cancelButtonText={i18n.translate('workspace.detail.dataSources.modal.cancelButton', { - defaultMessage: 'Cancel', - })} - confirmButtonText={i18n.translate('workspace.detail.dataSources.Modal.confirmButton', { - defaultMessage: 'Remove data source(s)', - })} - buttonColor="danger" - defaultFocusedButton="confirm" - /> - )} - + )} + itemId="id" + columns={columns} + isSelectable={true} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + isExpandable={true} + selection={selection} + /> ); }; diff --git a/src/plugins/workspace/public/components/workspace_form/index.ts b/src/plugins/workspace/public/components/workspace_form/index.ts index 79d934fb80b..bffeacbb0df 100644 --- a/src/plugins/workspace/public/components/workspace_form/index.ts +++ b/src/plugins/workspace/public/components/workspace_form/index.ts @@ -11,6 +11,7 @@ export { WorkspacePermissionSettingPanel } from './workspace_permission_setting_ export { WorkspaceCancelModal } from './workspace_cancel_modal'; export { WorkspaceNameField, WorkspaceDescriptionField } from './fields'; export { DirectQueryConnectionIcon } from './direct_query_connection_icon'; +export { DataSourceConnectionTable } from './data_source_connection_table'; export { WorkspaceFormSubmitData, WorkspaceFormProps, WorkspaceFormDataState } from './types'; export { diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index f9cef23b5a7..561dcd449a3 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import { EuiSpacer, EuiFlexItem, EuiSmallButton, EuiFlexGroup, EuiPanel } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { SavedObjectsStart, CoreStart } from '../../../../../core/public'; @@ -13,8 +13,8 @@ import { AssociationDataSourceModal } from '../workspace_detail/association_data import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceClient } from '../../workspace_client'; import { AssociationDataSourceModalMode } from '../../../common/constants'; -import { DataSourceConnectionTable } from '../workspace_detail/data_source_connection_table'; import { DataSourceEngineType } from '../../../../data_source/common/data_sources'; +import { DataSourceConnectionTable } from './data_source_connection_table'; export interface SelectDataSourcePanelProps { errors?: { [key: number]: WorkspaceFormError }; @@ -61,26 +61,29 @@ export const SelectDataSourcePanel = ({ }; const handleUnassignDataSources = (dataSourceConnections: DataSourceConnection[]) => { - const savedDataSourcesConnctions = (assignedDataSourceConnections ?? [])?.filter( + const savedDataSourcesConnections = (assignedDataSourceConnections ?? [])?.filter( ({ id }: DataSourceConnection) => !dataSourceConnections.some((item) => item.id === id) ); - setAssignedDataSourceConnections(savedDataSourcesConnctions); + setAssignedDataSourceConnections(savedDataSourcesConnections); const savedDataSources = (assignedDataSources ?? [])?.filter( ({ id }: DataSource) => !dataSourceConnections.some((item) => item.id === id) ); onChange(savedDataSources); }; + const handleSingleDataSourceUnAssign = (connection: DataSourceConnection) => { + handleUnassignDataSources([connection]); + }; + const renderTableContent = () => { return ( ); @@ -131,11 +134,6 @@ export const SelectDataSourcePanel = ({ ); - const getSelectedItems = useCallback( - (currentSelectedItems: DataSourceConnection[]) => setSelectedItems(currentSelectedItems), - [setSelectedItems] - ); - return (
From 64363b4af6123e471bee46b5676c33f9a617522d Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Wed, 4 Sep 2024 16:57:27 +0800 Subject: [PATCH 15/21] resolve some issues Signed-off-by: Kapian1234 --- .../workspace_creator/workspace_creator_form.tsx | 2 +- .../workspace_detail_connection_table.test.tsx | 3 ++- .../data_source_connection_table.tsx | 7 ++++++- .../select_data_source_panel.test.tsx | 4 ++-- .../workspace_form/select_data_source_panel.tsx | 15 +++++++-------- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx index bd60763564b..4c634e5286c 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx @@ -178,7 +178,7 @@ export const WorkspaceCreatorForm = (props: WorkspaceCreatorFormProps) => { savedObjects={savedObjects} assignedDataSources={formData.selectedDataSources} data-test-subj={`workspaceForm-dataSourcePanel`} - isDashboardAdmin={true} + showDataSourceManagement={true} /> diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.test.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.test.tsx index ce5f1b4df87..7a2bf370232 100644 --- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.test.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.test.tsx @@ -64,9 +64,10 @@ const dataSourceConnectionsMock = [ describe('WorkspaceDetailConnectionTable', () => { beforeEach(() => { + const mockPrepend = jest.fn().mockImplementation((path) => path); const mockHttp = { basePath: { - serverBasePath: '', + prepend: mockPrepend, }, }; (useOpenSearchDashboards as jest.Mock).mockImplementation(() => ({ diff --git a/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx b/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx index 0907324942b..90e80577540 100644 --- a/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx +++ b/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx @@ -118,9 +118,14 @@ export const DataSourceConnectionTable = ({ const basePath = http.basePath.serverBasePath; let url: string; if (record.connectionType === DataSourceConnectionType.OpenSearchConnection) { - url = `${origin}${basePath}/app/dataSources/${record.id}`; + url = http.basePath.prepend(`/app/dataSources/${record.id}`, { + withoutClientBasePath: true, + }); } else { url = `${origin}${basePath}/app/dataSources/manage/${name}?dataSourceMDSId=${record.parentId}`; + url = http.basePath.prepend(`/app/dataSources/${record.id}`, { + withoutClientBasePath: true, + }); } return ( 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 1a3f5e45608..fdb26c2b21f 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 @@ -80,7 +80,7 @@ const setup = ({ assignedDataSources = [], onChange = jest.fn(), errors = undefined, - isDashboardAdmin = true, + showDataSourceManagement = true, }: Partial) => { return render( @@ -90,7 +90,7 @@ const setup = ({ savedObjects={savedObjects} assignedDataSources={assignedDataSources} errors={errors} - isDashboardAdmin={isDashboardAdmin} + showDataSourceManagement={showDataSourceManagement} /> diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index 561dcd449a3..ec73a727d4f 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -21,15 +21,14 @@ export interface SelectDataSourcePanelProps { savedObjects: SavedObjectsStart; assignedDataSources: DataSource[]; onChange: (value: DataSource[]) => void; - isDashboardAdmin: boolean; + showDataSourceManagement: boolean; } export const SelectDataSourcePanel = ({ - errors, onChange, assignedDataSources, savedObjects, - isDashboardAdmin, + showDataSourceManagement, }: SelectDataSourcePanelProps) => { const [modalVisible, setModalVisible] = useState(false); const [selectedItems, setSelectedItems] = useState([]); @@ -79,7 +78,7 @@ export const SelectDataSourcePanel = ({ return ( - {isDashboardAdmin && selectedItems.length > 0 && assignedDataSources.length > 0 && ( + {showDataSourceManagement && selectedItems.length > 0 && assignedDataSources.length > 0 && ( {removeButton} )} - {isDashboardAdmin && ( + {showDataSourceManagement && ( {addOpenSearchConnectionsButton} )} - {isDashboardAdmin && ( + {showDataSourceManagement && ( {addDirectQueryConnectionsButton} )} @@ -161,7 +160,7 @@ export const SelectDataSourcePanel = ({ http={http} mode={toggleIdSelected as AssociationDataSourceModalMode} notifications={notifications} - logos={chrome?.logos} + logos={chrome.logos} /> )}
From 308c318d4a93501241b9a467e2ce56bde9ca0c6d Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Wed, 4 Sep 2024 17:00:32 +0800 Subject: [PATCH 16/21] resolve some issues Signed-off-by: Kapian1234 --- .../components/workspace_form/select_data_source_panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index ec73a727d4f..768f420d261 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -158,7 +158,7 @@ export const SelectDataSourcePanel = ({ closeModal={() => setModalVisible(false)} handleAssignDataSourceConnections={handleAssignDataSources} http={http} - mode={toggleIdSelected as AssociationDataSourceModalMode} + mode={toggleIdSelected} notifications={notifications} logos={chrome.logos} /> From 46e78c301f748607526390c0bef16538181368e3 Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Thu, 5 Sep 2024 11:45:35 +0800 Subject: [PATCH 17/21] Fix the data source URL reference Signed-off-by: Kapian1234 --- .../workspace_form/data_source_connection_table.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx b/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx index 7443cbb63f8..221fa44d7fd 100644 --- a/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx +++ b/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx @@ -114,18 +114,13 @@ export const DataSourceConnectionTable = ({ }), truncateText: true, render: (name: string, record) => { - const origin = window.location.origin; - const basePath = http.basePath.serverBasePath; let url: string; if (record.connectionType === DataSourceConnectionType.OpenSearchConnection) { - url = http.basePath.prepend(`/app/dataSources/${record.id}`, { - withoutClientBasePath: true, - }); + url = http.basePath.prepend(`/app/dataSources/${record.id}`); } else { - url = `${origin}${basePath}/app/dataSources/manage/${name}?dataSourceMDSId=${record.parentId}`; - url = http.basePath.prepend(`/app/dataSources/${record.id}`, { - withoutClientBasePath: true, - }); + url = http.basePath.prepend( + `/app/dataSources/manage/${name}?dataSourceMDSId=${record.parentId}` + ); } return ( From 1400600c3d8fa27d6aabc541174cd767bebc5a67 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 5 Sep 2024 11:36:53 +0800 Subject: [PATCH 18/21] Move restProps to tableProps Signed-off-by: Lin Wang --- .../workspace_detail_connection_table.tsx | 18 +++++++++------- .../data_source_connection_table.tsx | 21 +++++++------------ .../select_data_source_panel.tsx | 2 +- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.tsx index 6140b4d0c34..ca6ebe70a80 100644 --- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.tsx @@ -10,7 +10,7 @@ import { DataSourceConnection, DataSourceConnectionType } from '../../../common/ import { AssociationDataSourceModalMode } from '../../../common/constants'; import { DataSourceConnectionTable } from '../workspace_form'; -interface DataSourceConnectionTableProps { +interface WorkspaceDetailConnectionTableProps { isDashboardAdmin: boolean; connectionType: string; dataSourceConnections: DataSourceConnection[]; @@ -22,7 +22,7 @@ export const WorkspaceDetailConnectionTable = ({ connectionType, dataSourceConnections, handleUnassignDataSources, -}: DataSourceConnectionTableProps) => { +}: WorkspaceDetailConnectionTableProps) => { const [selectedItems, setSelectedItems] = useState([]); const [modalVisible, setModalVisible] = useState(false); @@ -82,18 +82,20 @@ export const WorkspaceDetailConnectionTable = ({ { { setSelectedItems([item]); setModalVisible(true); }} onSelectionChange={setSelectedItems} + tableProps={{ + search, + pagination: { + initialPageSize: 10, + pageSizeOptions: [10, 20, 30], + }, + }} /> } {modalVisible && ( diff --git a/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx b/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx index 221fa44d7fd..d440d3e751d 100644 --- a/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx +++ b/src/plugins/workspace/public/components/workspace_form/data_source_connection_table.tsx @@ -28,28 +28,22 @@ import { CoreStart } from '../../../../../core/public'; import './data_source_connection_table.scss'; -type DataSourceConnectionTableProps = Omit< - EuiInMemoryTableProps, - | 'columns' - | 'itemsId' - | 'isSelectable' - | 'itemIdToExpandedRowMap' - | 'isExpandable' - | 'selection' - | 'pagination' -> & { +interface DataSourceConnectionTableProps { isDashboardAdmin: boolean; connectionType: string; onUnlinkDataSource: (dataSources: DataSourceConnection) => void; onSelectionChange: (selections: DataSourceConnection[]) => void; -}; + dataSourceConnections: DataSourceConnection[]; + tableProps?: Pick, 'pagination' | 'search'>; +} export const DataSourceConnectionTable = ({ isDashboardAdmin, connectionType, onUnlinkDataSource, onSelectionChange, - ...restProps + tableProps, + dataSourceConnections, }: DataSourceConnectionTableProps) => { const [popoversState, setPopoversState] = useState>({}); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< @@ -240,7 +234,8 @@ export const DataSourceConnectionTable = ({ return ( )} + {...tableProps} + items={dataSourceConnections} itemId="id" columns={columns} isSelectable={true} diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index 768f420d261..07fbb7f69c4 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -79,7 +79,7 @@ export const SelectDataSourcePanel = ({ Date: Thu, 5 Sep 2024 11:45:17 +0800 Subject: [PATCH 19/21] Fix table not unmont after connection type changed Signed-off-by: Lin Wang --- .../workspace_detail/workspace_detail_connection_table.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.tsx index ca6ebe70a80..d3c52a9b613 100644 --- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail_connection_table.tsx @@ -96,6 +96,8 @@ export const WorkspaceDetailConnectionTable = ({ pageSizeOptions: [10, 20, 30], }, }} + /* Unmount table after connection type */ + key={connectionType} /> } {modalVisible && ( From 2451b927fd06e4de9a96ff022825443e1354b0af Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 5 Sep 2024 17:25:39 +0800 Subject: [PATCH 20/21] Refactor selectedDataSources to selectedDataSourceConnections Signed-off-by: Lin Wang --- .../workspace_creator/workspace_creator.tsx | 14 ++- .../workspace_creator_form.tsx | 7 +- .../workspace_form_summary_panel.test.tsx | 10 +- .../workspace_form_summary_panel.tsx | 4 +- .../select_data_source_panel.test.tsx | 66 +++++------ .../select_data_source_panel.tsx | 111 ++++++++++-------- .../workspace_detail.test.tsx | 8 +- .../workspace_detail/workspace_detail.tsx | 2 - .../components/workspace_detail_app.tsx | 44 ++++--- .../public/components/workspace_form/index.ts | 1 + .../select_data_source_panel.test.tsx | 63 ++++------ .../select_data_source_panel.tsx | 51 +++----- .../public/components/workspace_form/types.ts | 10 +- .../workspace_form/use_workspace_form.ts | 17 +-- .../components/workspace_form/utils.test.ts | 99 ++++++++++++++-- .../public/components/workspace_form/utils.ts | 60 +++++++--- .../workspace_form/workspace_form_context.tsx | 13 +- 17 files changed, 346 insertions(+), 234 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx index b8f32eb4a33..ed4370a7b3f 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -15,12 +15,12 @@ import { getUseCaseFeatureConfig } from '../../../common/utils'; import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; import { WorkspaceClient } from '../../workspace_client'; import { convertPermissionSettingsToPermissions } from '../workspace_form'; -import { DataSource } from '../../../common/types'; import { DataSourceManagementPluginSetup } from '../../../../../plugins/data_source_management/public'; import { WorkspaceUseCase } from '../../types'; import { getFirstUseCaseOfFeatureConfigs } from '../../utils'; import { useFormAvailableUseCases } from '../workspace_form/use_form_available_use_cases'; import { NavigationPublicPluginStart } from '../../../../../plugins/navigation/public'; +import { DataSourceConnectionType } from '../../../common/types'; import { WorkspaceCreatorForm } from './workspace_creator_form'; export interface WorkspaceCreatorProps { @@ -72,10 +72,14 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => { } setIsFormSubmitting(true); try { - const { permissionSettings, selectedDataSources, ...attributes } = data; - const selectedDataSourceIds = (selectedDataSources ?? []).map((ds: DataSource) => { - return ds.id; - }); + const { permissionSettings, selectedDataSourceConnections, ...attributes } = data; + const selectedDataSourceIds = (selectedDataSourceConnections ?? []) + .filter( + ({ connectionType }) => connectionType === DataSourceConnectionType.OpenSearchConnection + ) + .map(({ id }) => { + return id; + }); result = await workspaceClient.create(attributes, { dataSources: selectedDataSourceIds, permissions: convertPermissionSettingsToPermissions(permissionSettings), diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx index 4c634e5286c..4a99fc00652 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx @@ -58,7 +58,7 @@ export const WorkspaceCreatorForm = (props: WorkspaceCreatorFormProps) => { handleColorChange, handleUseCaseChange: handleUseCaseChangeInHook, setPermissionSettings, - setSelectedDataSources, + setSelectedDataSourceConnections, } = useWorkspaceForm(props); const nameManualChangedRef = useRef(false); @@ -173,10 +173,9 @@ export const WorkspaceCreatorForm = (props: WorkspaceCreatorFormProps) => { })} diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_form_summary_panel.test.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_form_summary_panel.test.tsx index 9ce350af85e..cfe9e583363 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_form_summary_panel.test.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_form_summary_panel.test.tsx @@ -20,10 +20,10 @@ describe('WorkspaceFormSummaryPanel', () => { name: 'Test Workspace', description: 'This is a test workspace', color: '#000000', - selectedDataSources: [ - { id: 'data-source-1', title: 'Data Source 1' }, - { id: 'data-source-2', title: 'Data Source 2' }, - { id: 'data-source-3', title: 'Data Source 3' }, + selectedDataSourceConnections: [ + { id: 'data-source-1', name: 'Data Source 1' }, + { id: 'data-source-2', name: 'Data Source 2' }, + { id: 'data-source-3', name: 'Data Source 3' }, ], permissionSettings: [ { id: 1, type: WorkspacePermissionItemType.User, userId: 'user1' }, @@ -74,7 +74,7 @@ describe('WorkspaceFormSummaryPanel', () => { - {formData.selectedDataSources.length > 0 && ( + {formData.selectedDataSourceConnections.length > 0 && ( title)} + texts={formData.selectedDataSourceConnections.map(({ name }) => name)} collapseDisplayCount={2} /> )} diff --git a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.test.tsx b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.test.tsx index 1329bef04be..2b2bef93f8d 100644 --- a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.test.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.test.tsx @@ -8,10 +8,13 @@ import React from 'react'; import { coreMock } from '../../../../../core/public/mocks'; import { createOpenSearchDashboardsReactContext } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceFormProvider, WorkspaceOperationType } from '../workspace_form'; -import { SelectDataSourceDetailPanel } from './select_data_source_panel'; +import { + SelectDataSourceDetailPanel, + SelectDataSourceDetailPanelProps, +} from './select_data_source_panel'; import * as utils from '../../utils'; import { IntlProvider } from 'react-intl'; -import { DataSourceConnectionType } from '../../../common/types'; +import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; const mockCoreStart = coreMock.createStart(); @@ -81,8 +84,7 @@ const defaultValues = { }; const defaultProps = { - savedObjects: {}, - assignedDataSources: [], + savedObjects: mockCoreStart.savedObjects, detailTitle: 'Data sources', isDashboardAdmin: true, currentWorkspace: workspaceObject, @@ -96,7 +98,14 @@ const success = jest.fn().mockResolvedValue({ }); const failed = jest.fn().mockResolvedValue({}); -const selectDataSourceDetailPanel = (props: any) => { +const selectDataSourceDetailPanel = ({ + action, + selectedDataSourceConnections, + ...props +}: { + action?: Function; + selectedDataSourceConnections?: DataSourceConnection[]; +} & Partial) => { const { Provider } = createOpenSearchDashboardsReactContext({ ...mockCoreStart, ...{ @@ -109,7 +118,7 @@ const selectDataSourceDetailPanel = (props: any) => { }, }, workspaceClient: { - update: props.action, + update: action, }, }, }); @@ -122,11 +131,16 @@ const selectDataSourceDetailPanel = (props: any) => { operationType={WorkspaceOperationType.Update} permissionEnabled={true} onSubmit={jest.fn()} - defaultValues={defaultValues} + defaultValues={{ ...defaultValues, selectedDataSourceConnections }} availableUseCases={[]} > - + @@ -134,12 +148,11 @@ const selectDataSourceDetailPanel = (props: any) => { }; describe('SelectDataSourceDetailPanel', () => { - afterEach(() => { + beforeEach(() => { jest.clearAllMocks(); }); it('should show message when no data sources are assigned', async () => { - jest.spyOn(utils, 'fetchDataSourceConnections').mockResolvedValue([]); const { getByText, getAllByText } = render(selectDataSourceDetailPanel(defaultProps)); await waitFor(() => { expect(getByText('No data sources to display')).toBeInTheDocument(); @@ -151,7 +164,6 @@ describe('SelectDataSourceDetailPanel', () => { }); it('should not show assocition button when user is not OSD admin', async () => { - jest.spyOn(utils, 'fetchDataSourceConnections').mockResolvedValue([]); const { getByText, queryByText } = render( selectDataSourceDetailPanel({ ...defaultProps, @@ -167,11 +179,9 @@ describe('SelectDataSourceDetailPanel', () => { }); it('should not show remove associations button when user is not OSD admin', async () => { - jest.spyOn(utils, 'fetchDataSourceConnections').mockResolvedValue(dataSourceConnectionsMock); const { queryByTestId } = render( selectDataSourceDetailPanel({ ...defaultProps, - assignedDataSources: dataSources, isDashboardAdmin: false, }) ); @@ -181,7 +191,6 @@ describe('SelectDataSourceDetailPanel', () => { }); it('should switch toggle button', async () => { - jest.spyOn(utils, 'fetchDataSourceConnections').mockResolvedValue(dataSourceConnectionsMock); const { getByText } = render(selectDataSourceDetailPanel(defaultProps)); await waitFor(() => { const dqcButton = getByText('Direct query connections'); @@ -200,7 +209,6 @@ describe('SelectDataSourceDetailPanel', () => { value: 600, }); jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue([]); - jest.spyOn(utils, 'fetchDataSourceConnections').mockResolvedValueOnce([]); jest .spyOn(utils, 'fetchDataSourceConnections') .mockResolvedValueOnce(dataSourceConnectionsMock); @@ -245,7 +253,6 @@ describe('SelectDataSourceDetailPanel', () => { value: 600, }); jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue([]); - jest.spyOn(utils, 'fetchDataSourceConnections').mockResolvedValueOnce([]); jest .spyOn(utils, 'fetchDataSourceConnections') .mockResolvedValueOnce(dataSourceConnectionsMock); @@ -317,13 +324,10 @@ describe('SelectDataSourceDetailPanel', () => { }); it('should success to remove data sources', async () => { - jest - .spyOn(utils, 'fetchDataSourceConnections') - .mockResolvedValueOnce([dataSourceConnectionsMock[0]]); const { getByText, getByTestId, getByRole } = render( selectDataSourceDetailPanel({ ...defaultProps, - assignedDataSources: [dataSources[0]], + selectedDataSourceConnections: [dataSourceConnectionsMock[0]], action: success, }) ); @@ -341,13 +345,10 @@ describe('SelectDataSourceDetailPanel', () => { }); it('should fail to remove data sources', async () => { - jest - .spyOn(utils, 'fetchDataSourceConnections') - .mockResolvedValueOnce([dataSourceConnectionsMock[0]]); const { getByText, getByTestId, getByRole } = render( selectDataSourceDetailPanel({ ...defaultProps, - assignedDataSources: [dataSources[0]], + selectedDataSourceConnections: [dataSourceConnectionsMock[0]], action: failed, }) ); @@ -365,13 +366,10 @@ describe('SelectDataSourceDetailPanel', () => { }); it('should remove selected data sources successfully', async () => { - jest - .spyOn(utils, 'fetchDataSourceConnections') - .mockResolvedValueOnce([dataSourceConnectionsMock[0]]); const { getByText, queryByTestId, getAllByRole, getByRole } = render( selectDataSourceDetailPanel({ ...defaultProps, - assignedDataSources: [dataSources[0]], + selectedDataSourceConnections: [dataSourceConnectionsMock[0]], action: success, }) ); @@ -392,20 +390,22 @@ describe('SelectDataSourceDetailPanel', () => { }); it('should handle input in the search box', async () => { - jest.spyOn(utils, 'fetchDataSourceConnections').mockResolvedValue(dataSourceConnectionsMock); const { getByText, queryByText } = render( selectDataSourceDetailPanel({ ...defaultProps, - assignedDataSources: dataSources, + selectedDataSourceConnections: dataSourceConnectionsMock, }) ); await waitFor(() => { expect(getByText('Data Source 1')).toBeInTheDocument(); expect(getByText('Data Source 2')).toBeInTheDocument(); + }); - const searchInput = screen.getByPlaceholderText('Search...'); - // Simulate typing in the search input - fireEvent.change(searchInput, { target: { value: 'Data Source 1' } }); + const searchInput = screen.getByPlaceholderText('Search...'); + // Simulate typing in the search input + fireEvent.change(searchInput, { target: { value: 'Data Source 1' } }); + + await waitFor(() => { expect(getByText('Data Source 1')).toBeInTheDocument(); expect(queryByText('Data Source 2')).toBeNull(); }); diff --git a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx index 17abe39fdda..95f463d9f87 100644 --- a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { EuiText, EuiTitle, @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from 'react-intl'; -import { DataSource, DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; +import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { WorkspaceClient } from '../../workspace_client'; import { WorkspaceDetailConnectionTable } from './workspace_detail_connection_table'; import { AssociationDataSourceModal } from './association_data_source_modal'; @@ -32,8 +32,11 @@ import { WorkspaceObject, ChromeStart, } from '../../../../../core/public'; -import { convertPermissionSettingsToPermissions, useWorkspaceFormContext } from '../workspace_form'; -import { fetchDataSourceConnections } from '../../utils'; +import { + convertPermissionSettingsToPermissions, + isWorkspacePermissionSetting, + useWorkspaceFormContext, +} from '../workspace_form'; import { AssociationDataSourceModalMode } from '../../../common/constants'; const toggleButtons: EuiButtonGroupOptionProps[] = [ @@ -50,9 +53,8 @@ const toggleButtons: EuiButtonGroupOptionProps[] = [ }), }, ]; -export interface SelectDataSourcePanelProps { +export interface SelectDataSourceDetailPanelProps { savedObjects: SavedObjectsStart; - assignedDataSources: DataSource[]; detailTitle: string; isDashboardAdmin: boolean; currentWorkspace: WorkspaceObject; @@ -60,56 +62,48 @@ export interface SelectDataSourcePanelProps { } export const SelectDataSourceDetailPanel = ({ - assignedDataSources, savedObjects, detailTitle, isDashboardAdmin, currentWorkspace, chrome, -}: SelectDataSourcePanelProps) => { +}: SelectDataSourceDetailPanelProps) => { const { services: { notifications, workspaceClient, http }, } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); - const { formData, setSelectedDataSources } = useWorkspaceFormContext(); + const { formData, setSelectedDataSourceConnections } = useWorkspaceFormContext(); const [isLoading, setIsLoading] = useState(false); const [isVisible, setIsVisible] = useState(false); - const [assignedDataSourceConnections, setAssignedDataSourceConnections] = useState< - DataSourceConnection[] - >([]); const [toggleIdSelected, setToggleIdSelected] = useState(toggleButtons[0].id); - useEffect(() => { - setIsLoading(true); - fetchDataSourceConnections(assignedDataSources, http, notifications).then((connections) => { - setAssignedDataSourceConnections(connections); - setIsLoading(false); - }); - }, [assignedDataSources, http, notifications]); - const handleAssignDataSourceConnections = async ( - dataSourceConnections: DataSourceConnection[] + newAssignedDataSourceConnections: DataSourceConnection[] ) => { - const dataSources = dataSourceConnections - .filter( - ({ connectionType }) => connectionType === DataSourceConnectionType.OpenSearchConnection - ) - .map(({ id, type, name, description }) => ({ - id, - title: name, - description, - dataSourceEngineType: type, - })); try { setIsLoading(true); setIsVisible(false); - const { permissionSettings, selectedDataSources, useCase, ...attributes } = formData; - const savedDataSources: DataSource[] = [...selectedDataSources, ...dataSources]; + const { + permissionSettings, + selectedDataSourceConnections, + useCase, + ...attributes + } = formData; + + const savedDataSourceConnections = [ + ...formData.selectedDataSourceConnections, + ...newAssignedDataSourceConnections, + ]; const result = await workspaceClient.update(currentWorkspace.id, attributes, { - dataSources: savedDataSources.map((ds) => { - return ds.id; - }), - permissions: convertPermissionSettingsToPermissions(permissionSettings), + dataSources: savedDataSourceConnections + .filter( + ({ connectionType }) => connectionType === DataSourceConnectionType.OpenSearchConnection + ) + .map(({ id }) => id), + // Todo: Make permissions be an optional parameter when update workspace + permissions: convertPermissionSettingsToPermissions( + permissionSettings.filter(isWorkspacePermissionSetting) + ), }); if (result?.success) { notifications?.toasts.addSuccess({ @@ -117,7 +111,7 @@ export const SelectDataSourceDetailPanel = ({ defaultMessage: 'Associate OpenSearch connections successfully', }), }); - setSelectedDataSources(savedDataSources); + setSelectedDataSourceConnections(savedDataSourceConnections); } else { throw new Error(result?.error ? result?.error : 'Associate OpenSearch connections failed'); } @@ -134,17 +128,30 @@ export const SelectDataSourceDetailPanel = ({ }; const handleUnassignDataSources = useCallback( - async (dataSources: DataSourceConnection[]) => { + async (unAssignedDataSources: DataSourceConnection[]) => { try { setIsLoading(true); - const { permissionSettings, selectedDataSources, useCase, ...attributes } = formData; - const savedDataSources = (selectedDataSources ?? [])?.filter( - ({ id }: DataSource) => !dataSources.some((item) => item.id === id) + const { + permissionSettings, + selectedDataSourceConnections, + useCase, + ...attributes + } = formData; + const savedDataSourceConnections = selectedDataSourceConnections.filter( + ({ id }) => !unAssignedDataSources.some((item) => item.id === id) ); const result = await workspaceClient.update(currentWorkspace.id, attributes, { - dataSources: savedDataSources.map(({ id }: DataSource) => id), - permissions: convertPermissionSettingsToPermissions(permissionSettings), + dataSources: savedDataSourceConnections + .filter( + ({ connectionType }) => + connectionType === DataSourceConnectionType.OpenSearchConnection + ) + .map(({ id }) => id), + // Todo: Make permissions be an optional parameter when update workspace + permissions: convertPermissionSettingsToPermissions( + permissionSettings.filter(isWorkspacePermissionSetting) + ), }); if (result?.success) { notifications?.toasts.addSuccess({ @@ -152,7 +159,7 @@ export const SelectDataSourceDetailPanel = ({ defaultMessage: 'Remove associated OpenSearch connections successfully', }), }); - setSelectedDataSources(savedDataSources); + setSelectedDataSourceConnections(savedDataSourceConnections); } else { throw new Error( result?.error ? result?.error : 'Remove associated OpenSearch connections failed' @@ -169,7 +176,13 @@ export const SelectDataSourceDetailPanel = ({ setIsLoading(false); } }, - [currentWorkspace.id, formData, notifications?.toasts, setSelectedDataSources, workspaceClient] + [ + currentWorkspace.id, + formData, + notifications?.toasts, + setSelectedDataSourceConnections, + workspaceClient, + ] ); const associationButton = ( @@ -240,14 +253,14 @@ export const SelectDataSourceDetailPanel = ({ if (isLoading) { return loadingMessage; } - if (assignedDataSources.length === 0) { + if (formData.selectedDataSourceConnections.length === 0) { return noAssociationMessage; } return ( ); @@ -284,7 +297,7 @@ export const SelectDataSourceDetailPanel = ({ notifications={notifications} savedObjects={savedObjects} closeModal={() => setIsVisible(false)} - assignedConnections={assignedDataSourceConnections} + assignedConnections={formData.selectedDataSourceConnections} handleAssignDataSourceConnections={handleAssignDataSourceConnections} mode={toggleIdSelected as AssociationDataSourceModalMode} logos={chrome.logos} diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx index 6004e4de206..0a528f433b8 100644 --- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx @@ -13,6 +13,7 @@ import { createMockedRegisteredUseCases$ } from '../../mocks'; import { WorkspaceDetail } from './workspace_detail'; import { WorkspaceFormProvider, WorkspaceOperationType } from '../workspace_form'; import { MemoryRouter } from 'react-router-dom'; +import { DataSourceConnectionType } from '../../../common/types'; // all applications const PublicAPPInfoMap = new Map([ @@ -52,12 +53,13 @@ const defaultValues = { modes: ['library_write', 'write'], }, ], - selectedDataSources: [ + selectedDataSourceConnections: [ { id: 'ds-1', - title: 'ds-1-title', + name: 'ds-1-title', description: 'ds-1-description', - dataSourceEngineType: 'OpenSearch', + type: 'OpenSearch', + connectionType: DataSourceConnectionType.OpenSearchConnection, }, ], }; diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx index 3543795f7e4..6a7d6955266 100644 --- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx @@ -58,7 +58,6 @@ export const WorkspaceDetail = (props: WorkspaceDetailProps) => { }>(); const { - formData, isEditing, formId, numberOfErrors, @@ -142,7 +141,6 @@ export const WorkspaceDetail = (props: WorkspaceDetailProps) => { content: ( & { - selectedDataSources: DataSource[]; -}; - export const WorkspaceDetailApp = (props: WorkspaceDetailProps) => { const { services: { @@ -56,7 +53,9 @@ export const WorkspaceDetailApp = (props: WorkspaceDetailProps) => { http, }, } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); - const [currentWorkspaceFormData, setCurrentWorkspaceFormData] = useState(); + const [currentWorkspaceFormData, setCurrentWorkspaceFormData] = useState< + WorkspaceFormSubmitData + >(); const currentWorkspace = useObservable(workspaces ? workspaces.currentWorkspace$ : of(null)); const availableUseCases = useObservable(props.registeredUseCases$, []); const isPermissionEnabled = application?.capabilities.workspaces.permissionEnabled; @@ -93,14 +92,21 @@ export const WorkspaceDetailApp = (props: WorkspaceDetailProps) => { const rawFormData = getFormDataFromWorkspace(currentWorkspace); if (rawFormData && savedObjects && currentWorkspace) { - getDataSourcesList(savedObjects.client, [currentWorkspace.id]).then((selectedDataSources) => { - setCurrentWorkspaceFormData({ - ...rawFormData, - selectedDataSources, + getDataSourcesList(savedObjects.client, [currentWorkspace.id]) + .then((assignedDataSources) => + fetchDataSourceConnections(assignedDataSources, http, notifications) + ) + .then((selectedDataSourceConnections) => { + setCurrentWorkspaceFormData({ + ...rawFormData, + // Connection with parent id means DQC, should filter them out + selectedDataSourceConnections: selectedDataSourceConnections.filter( + ({ parentId }) => !parentId + ), + }); }); - }); } - }, [currentWorkspace, savedObjects]); + }, [currentWorkspace, savedObjects, http, notifications]); const handleWorkspaceFormSubmit = useCallback( async (data: WorkspaceFormSubmitData) => { @@ -115,10 +121,14 @@ export const WorkspaceDetailApp = (props: WorkspaceDetailProps) => { } try { - const { permissionSettings, selectedDataSources, ...attributes } = data; - const selectedDataSourceIds = (selectedDataSources ?? []).map((ds: DataSource) => { - return ds.id; - }); + const { permissionSettings, selectedDataSourceConnections, ...attributes } = data; + const selectedDataSourceIds = (selectedDataSourceConnections ?? []) + .filter( + ({ connectionType }) => connectionType === DataSourceConnectionType.OpenSearchConnection + ) + .map((connection) => { + return connection.id; + }); result = await workspaceClient.update(currentWorkspace.id, attributes, { dataSources: selectedDataSourceIds, diff --git a/src/plugins/workspace/public/components/workspace_form/index.ts b/src/plugins/workspace/public/components/workspace_form/index.ts index bffeacbb0df..2fae243a37a 100644 --- a/src/plugins/workspace/public/components/workspace_form/index.ts +++ b/src/plugins/workspace/public/components/workspace_form/index.ts @@ -27,6 +27,7 @@ export { export { convertPermissionsToPermissionSettings, convertPermissionSettingsToPermissions, + isWorkspacePermissionSetting, } from './utils'; export { WorkspaceFormProvider, useWorkspaceFormContext } from './workspace_form_context'; 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 fdb26c2b21f..460921530d4 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 @@ -77,7 +77,7 @@ const mockCoreStart = coreMock.createStart(); const setup = ({ savedObjects = mockCoreStart.savedObjects, - assignedDataSources = [], + assignedDataSourceConnections = [], onChange = jest.fn(), errors = undefined, showDataSourceManagement = true, @@ -88,7 +88,7 @@ const setup = ({ @@ -126,21 +126,19 @@ describe('SelectDataSourcePanel', () => { ); }); it('should render consistent data sources when selected data sources passed', async () => { - const { getByText, getByTestId } = setup({ assignedDataSources: dataSources }); - fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); + const { getByText, getByTestId, queryByText } = setup({ + assignedDataSourceConnections: [assignedDataSourcesConnections[0]], + }); await waitFor(() => { expect(getByText(assignedDataSourcesConnections[0].name)).toBeInTheDocument(); - expect(getByText(assignedDataSourcesConnections[1].name)).toBeInTheDocument(); + expect(queryByText(assignedDataSourcesConnections[1].name)).not.toBeInTheDocument(); }); - fireEvent.click(getByText(assignedDataSourcesConnections[0].name)); - fireEvent.click(getByText(assignedDataSourcesConnections[1].name)); - fireEvent.click(getByText('Associate data sources')); + fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); await waitFor(() => { - expect(getByText(dataSources[0].title)).toBeInTheDocument(); - expect(getByText(dataSources[1].title)).toBeInTheDocument(); + expect(getByText(assignedDataSourcesConnections[1].name)).toBeInTheDocument(); }); }); @@ -148,7 +146,7 @@ describe('SelectDataSourcePanel', () => { const onChangeMock = jest.fn(); const { getByTestId, getByText } = setup({ onChange: onChangeMock, - assignedDataSources: [], + assignedDataSourceConnections: [], }); expect(onChangeMock).not.toHaveBeenCalled(); @@ -188,7 +186,7 @@ describe('SelectDataSourcePanel', () => { const onChangeMock = jest.fn(); const { getByText, getByTestId } = setup({ onChange: onChangeMock, - assignedDataSources: dataSources, + assignedDataSourceConnections: assignedDataSourcesConnections, }); fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); @@ -208,36 +206,27 @@ describe('SelectDataSourcePanel', () => { fireEvent.click(getByTestId('checkboxSelectRow-' + dataSources[1].id)); fireEvent.click(getByText('Remove selected')); }); - expect(onChangeMock).toHaveBeenCalledWith([dataSources[0]]); - }); -}); - -it('should close associate data sources modal', async () => { - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { - configurable: true, - value: 600, - }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { - configurable: true, - value: 600, + expect(onChangeMock).toHaveBeenCalledWith([assignedDataSourcesConnections[0]]); }); - const { getByText, queryByText, getByTestId } = setup({ - assignedDataSources: [], - }); + it('should close associate data sources modal', async () => { + const { getByText, queryByText, getByTestId } = setup({ + assignedDataSourceConnections: [], + }); - fireEvent.click(getByTestId('workspace-creator-dataSources-assign-button')); - await waitFor(() => { + 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 connection, they will also be available in the workspace.' + ) + ).toBeInTheDocument(); + }); + fireEvent.click(getByText('Close')); expect( - getByText( + queryByText( 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query connection, they will also be available in the workspace.' ) - ).toBeInTheDocument(); + ).toBeNull(); }); - fireEvent.click(getByText('Close')); - expect( - queryByText( - 'Add data sources that will be available in the workspace. If a selected data source has related Direct Query connection, they will also be available in the workspace.' - ) - ).toBeNull(); }); diff --git a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx index 07fbb7f69c4..b1ec518d77c 100644 --- a/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/select_data_source_panel.tsx @@ -7,34 +7,30 @@ import React, { useState } from 'react'; import { EuiSpacer, EuiFlexItem, EuiSmallButton, EuiFlexGroup, EuiPanel } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { SavedObjectsStart, CoreStart } from '../../../../../core/public'; -import { DataSource, DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; +import { DataSourceConnection } from '../../../common/types'; import { WorkspaceFormError } from './types'; import { AssociationDataSourceModal } from '../workspace_detail/association_data_source_modal'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceClient } from '../../workspace_client'; import { AssociationDataSourceModalMode } from '../../../common/constants'; -import { DataSourceEngineType } from '../../../../data_source/common/data_sources'; import { DataSourceConnectionTable } from './data_source_connection_table'; export interface SelectDataSourcePanelProps { errors?: { [key: number]: WorkspaceFormError }; savedObjects: SavedObjectsStart; - assignedDataSources: DataSource[]; - onChange: (value: DataSource[]) => void; + assignedDataSourceConnections: DataSourceConnection[]; + onChange: (value: DataSourceConnection[]) => void; showDataSourceManagement: boolean; } export const SelectDataSourcePanel = ({ onChange, - assignedDataSources, + assignedDataSourceConnections, savedObjects, showDataSourceManagement, }: SelectDataSourcePanelProps) => { const [modalVisible, setModalVisible] = useState(false); const [selectedItems, setSelectedItems] = useState([]); - const [assignedDataSourceConnections, setAssignedDataSourceConnections] = useState< - DataSourceConnection[] - >([]); const [toggleIdSelected, setToggleIdSelected] = useState( AssociationDataSourceModalMode.OpenSearchConnections ); @@ -42,32 +38,17 @@ export const SelectDataSourcePanel = ({ services: { notifications, http, chrome }, } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); - const handleAssignDataSources = (dataSourceConnections: DataSourceConnection[]) => { - setAssignedDataSourceConnections([...assignedDataSourceConnections, ...dataSourceConnections]); + const handleAssignDataSourceConnections = (newDataSourceConnections: DataSourceConnection[]) => { setModalVisible(false); - const dataSources = dataSourceConnections - .filter( - ({ connectionType }) => connectionType === DataSourceConnectionType.OpenSearchConnection - ) - .map(({ id, type, name, description }) => ({ - id, - title: name, - description, - dataSourceEngineType: type as DataSourceEngineType, - })); - const savedDataSources: DataSource[] = [...assignedDataSources, ...dataSources]; - onChange(savedDataSources); + onChange([...assignedDataSourceConnections, ...newDataSourceConnections]); }; const handleUnassignDataSources = (dataSourceConnections: DataSourceConnection[]) => { - const savedDataSourcesConnections = (assignedDataSourceConnections ?? [])?.filter( - ({ id }: DataSourceConnection) => !dataSourceConnections.some((item) => item.id === id) - ); - setAssignedDataSourceConnections(savedDataSourcesConnections); - const savedDataSources = (assignedDataSources ?? [])?.filter( - ({ id }: DataSource) => !dataSourceConnections.some((item) => item.id === id) + onChange( + assignedDataSourceConnections.filter( + ({ id }: DataSourceConnection) => !dataSourceConnections.some((item) => item.id === id) + ) ); - onChange(savedDataSources); }; const handleSingleDataSourceUnAssign = (connection: DataSourceConnection) => { @@ -137,9 +118,11 @@ export const SelectDataSourcePanel = ({
- {showDataSourceManagement && selectedItems.length > 0 && assignedDataSources.length > 0 && ( - {removeButton} - )} + {showDataSourceManagement && + selectedItems.length > 0 && + assignedDataSourceConnections.length > 0 && ( + {removeButton} + )} {showDataSourceManagement && ( {addOpenSearchConnectionsButton} )} @@ -149,14 +132,14 @@ export const SelectDataSourcePanel = ({ - {assignedDataSources.length > 0 && renderTableContent()} + {assignedDataSourceConnections.length > 0 && renderTableContent()} {modalVisible && chrome && ( setModalVisible(false)} - handleAssignDataSourceConnections={handleAssignDataSources} + handleAssignDataSourceConnections={handleAssignDataSourceConnections} http={http} mode={toggleIdSelected} notifications={notifications} diff --git a/src/plugins/workspace/public/components/workspace_form/types.ts b/src/plugins/workspace/public/components/workspace_form/types.ts index 0bf8881cd3b..7ca175f794b 100644 --- a/src/plugins/workspace/public/components/workspace_form/types.ts +++ b/src/plugins/workspace/public/components/workspace_form/types.ts @@ -6,7 +6,7 @@ import type { ApplicationStart, SavedObjectsStart } from '../../../../../core/public'; import type { WorkspacePermissionMode } from '../../../common/constants'; import type { WorkspaceOperationType, WorkspacePermissionItemType } from './constants'; -import { DataSource } from '../../../common/types'; +import { DataSourceConnection } from '../../../common/types'; import { DataSourceManagementPluginSetup } from '../../../../../plugins/data_source_management/public'; import { WorkspaceUseCase } from '../../types'; @@ -34,7 +34,7 @@ export interface WorkspaceFormSubmitData { features: string[]; color?: string; permissionSettings?: WorkspacePermissionSetting[]; - selectedDataSources?: DataSource[]; + selectedDataSourceConnections?: DataSourceConnection[]; } export enum WorkspaceFormErrorCode { @@ -61,14 +61,14 @@ export interface WorkspaceFormError { export type WorkspaceFormErrors = { [key in keyof Omit< WorkspaceFormSubmitData, - 'permissionSettings' | 'description' | 'selectedDataSources' + 'permissionSettings' | 'description' | 'selectedDataSourceConnections' >]?: WorkspaceFormError; } & { permissionSettings?: { overall?: WorkspaceFormError; fields?: { [key: number]: WorkspaceFormError }; }; - selectedDataSources?: { [key: number]: WorkspaceFormError }; + selectedDataSourceConnections?: { [key: number]: WorkspaceFormError }; }; export interface WorkspaceFormProps { @@ -91,7 +91,7 @@ export interface WorkspaceFormDataState extends Omit { name: string; useCase: string | undefined; - selectedDataSources: DataSource[]; + selectedDataSourceConnections: DataSourceConnection[]; permissionSettings: Array< Pick & Partial >; diff --git a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts index 703255455ec..74cf11982f4 100644 --- a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts +++ b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts @@ -8,7 +8,7 @@ import { htmlIdGenerator, EuiColorPickerProps } from '@elastic/eui'; import { useApplications } from '../../hooks'; import { getFirstUseCaseOfFeatureConfigs, isUseCaseFeatureConfig } from '../../utils'; -import { DataSource } from '../../../common/types'; +import { DataSourceConnection } from '../../../common/types'; import { getUseCaseFeatureConfig } from '../../../common/utils'; import { WorkspaceFormProps, @@ -51,9 +51,12 @@ export const useWorkspaceForm = ({ WorkspaceFormDataState['permissionSettings'] >(initialPermissionSettingsRef.current); - const [selectedDataSources, setSelectedDataSources] = useState( - defaultValues?.selectedDataSources && defaultValues.selectedDataSources.length > 0 - ? defaultValues.selectedDataSources + const [selectedDataSourceConnections, setSelectedDataSourceConnections] = useState< + DataSourceConnection[] + >( + defaultValues?.selectedDataSourceConnections && + defaultValues.selectedDataSourceConnections.length > 0 + ? defaultValues.selectedDataSourceConnections : [] ); @@ -67,7 +70,7 @@ export const useWorkspaceForm = ({ useCase: selectedUseCase, color, permissionSettings, - selectedDataSources, + selectedDataSourceConnections, }); const getFormDataRef = useRef(getFormData); getFormDataRef.current = getFormData; @@ -122,7 +125,7 @@ export const useWorkspaceForm = ({ color: currentFormData.color || '#FFFFFF', features: currentFormData.features, permissionSettings: currentFormData.permissionSettings as WorkspacePermissionSetting[], - selectedDataSources: currentFormData.selectedDataSources, + selectedDataSourceConnections: currentFormData.selectedDataSourceConnections, }); }, [onSubmit, permissionEnabled] @@ -159,6 +162,6 @@ export const useWorkspaceForm = ({ handleColorChange, handleUseCaseChange, setPermissionSettings, - setSelectedDataSources, + setSelectedDataSourceConnections, }; }; diff --git a/src/plugins/workspace/public/components/workspace_form/utils.test.ts b/src/plugins/workspace/public/components/workspace_form/utils.test.ts index 3a45165044d..03cea502f57 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.test.ts @@ -9,9 +9,15 @@ import { convertPermissionsToPermissionSettings, getNumberOfChanges, getNumberOfErrors, + isWorkspacePermissionSetting, } from './utils'; import { WorkspacePermissionMode } from '../../../common/constants'; -import { WorkspacePermissionItemType } from './constants'; +import { + WorkspacePermissionItemType, + optionIdToWorkspacePermissionModesMap, + PermissionModeId, +} from './constants'; +import { DataSourceConnectionType } from '../../../common/types'; import { WorkspaceFormErrorCode } from './types'; describe('convertPermissionSettingsToPermissions', () => { @@ -337,15 +343,17 @@ describe('validateWorkspaceForm', () => { validateWorkspaceForm( { name: 'test', - selectedDataSources: [ + selectedDataSourceConnections: [ { id: '', - title: 'title', + name: 'title', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', }, ], }, false - ).selectedDataSources + ).selectedDataSourceConnections ).toEqual({ 0: { code: WorkspaceFormErrorCode.InvalidDataSource, message: 'Invalid data source' }, }); @@ -356,19 +364,23 @@ describe('validateWorkspaceForm', () => { validateWorkspaceForm( { name: 'test', - selectedDataSources: [ + selectedDataSourceConnections: [ { id: 'id', - title: 'title1', + name: 'title1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', }, { id: 'id', - title: 'title2', + name: 'title2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', }, ], }, false - ).selectedDataSources + ).selectedDataSourceConnections ).toEqual({ '1': { code: WorkspaceFormErrorCode.DuplicateDataSource, message: 'Duplicate data sources' }, }); @@ -379,7 +391,7 @@ describe('getNumberOfErrors', () => { it('should calculate the error number of data sources form', () => { expect( getNumberOfErrors({ - selectedDataSources: { + selectedDataSourceConnections: { 0: { code: WorkspaceFormErrorCode.InvalidDataSource, message: 'Invalid data source' }, }, }) @@ -617,3 +629,72 @@ describe('getNumberOfChanges', () => { ).toEqual(3); }); }); + +describe('isWorkspacePermissionSetting', () => { + it('should return true for a valid user permission setting', () => { + const validUserPermissionSetting = { + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + type: WorkspacePermissionItemType.User, + userId: 'user123', + }; + const result = isWorkspacePermissionSetting(validUserPermissionSetting); + expect(result).toBe(true); + }); + + it('should return true for a valid group permission setting', () => { + const validGroupPermissionSetting = { + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Owner], + type: WorkspacePermissionItemType.Group, + group: 'group456', + }; + const result = isWorkspacePermissionSetting(validGroupPermissionSetting); + expect(result).toBe(true); + }); + + it('should return false if modes is missing', () => { + const permissionSettingWithoutModes = { + type: WorkspacePermissionItemType.User, + userId: 'user123', + }; + const result = isWorkspacePermissionSetting(permissionSettingWithoutModes); + expect(result).toBe(false); + }); + + it('should return false if modes are invalid', () => { + const permissionSettingWithInvalidModes = { + modes: ['invalid' as WorkspacePermissionMode], + type: WorkspacePermissionItemType.User, + userId: 'user123', + }; + const result = isWorkspacePermissionSetting(permissionSettingWithInvalidModes); + expect(result).toBe(false); + }); + + it('should return false if type is invalid', () => { + const permissionSettingWithInvalidType = { + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Owner], + type: 'invalid', + userId: 'user123', + }; + const result = isWorkspacePermissionSetting(permissionSettingWithInvalidType); + expect(result).toBe(false); + }); + + it('should return false if userId is missing for user type', () => { + const permissionSettingWithoutUserId = { + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Owner], + type: WorkspacePermissionItemType.User, + }; + const result = isWorkspacePermissionSetting(permissionSettingWithoutUserId); + expect(result).toBe(false); + }); + + it('should return false if group is missing for group type', () => { + const permissionSettingWithoutGroup = { + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Owner], + type: WorkspacePermissionItemType.Group, + }; + const result = isWorkspacePermissionSetting(permissionSettingWithoutGroup); + expect(result).toBe(false); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts index 161def1f3f6..5e03c724889 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.ts @@ -25,7 +25,7 @@ import { WorkspaceUserGroupPermissionSetting, WorkspaceUserPermissionSetting, } from './types'; -import { DataSource } from '../../../common/types'; +import { DataSourceConnection } from '../../../common/types'; import { validateWorkspaceColor } from '../../../common/utils'; export const isValidFormTextInput = (input?: string) => { @@ -48,8 +48,8 @@ export const getNumberOfErrors = (formErrors: WorkspaceFormErrors) => { if (formErrors.permissionSettings?.overall) { numberOfErrors += 1; } - if (formErrors.selectedDataSources) { - numberOfErrors += Object.keys(formErrors.selectedDataSources).length; + if (formErrors.selectedDataSourceConnections) { + numberOfErrors += Object.keys(formErrors.selectedDataSourceConnections).length; } if (formErrors.features) { numberOfErrors += 1; @@ -226,7 +226,7 @@ const validateUserGroupPermissionSetting = ( } }; -const validatePermissionSetting = ( +const validatePermissionSettings = ( permissionSettings?: Array< Pick & Partial > @@ -289,17 +289,17 @@ const validatePermissionSetting = ( : {}), }; }; -export const isSelectedDataSourcesDuplicated = ( - selectedDataSources: DataSource[], - row: DataSource -) => selectedDataSources.some((ds) => ds.id === row.id); +export const isSelectedDataSourceConnectionsDuplicated = ( + selectedDataSourceConnections: DataSourceConnection[], + row: DataSourceConnection +) => selectedDataSourceConnections.some((connection) => connection.id === row.id); export const validateWorkspaceForm = ( formData: Partial, isPermissionEnabled: boolean ) => { const formErrors: WorkspaceFormErrors = {}; - const { name, permissionSettings, color, features, selectedDataSources } = formData; + const { name, permissionSettings, color, features, selectedDataSourceConnections } = formData; if (name && name.trim()) { if (!isValidFormTextInput(name)) { formErrors.name = { @@ -334,12 +334,12 @@ export const validateWorkspaceForm = ( }; } if (isPermissionEnabled) { - formErrors.permissionSettings = validatePermissionSetting(permissionSettings); + formErrors.permissionSettings = validatePermissionSettings(permissionSettings); } - if (selectedDataSources) { + if (selectedDataSourceConnections) { const dataSourcesErrors: { [key: number]: WorkspaceFormError } = {}; - for (let i = 0; i < selectedDataSources.length; i++) { - const row = selectedDataSources[i]; + for (let i = 0; i < selectedDataSourceConnections.length; i++) { + const row = selectedDataSourceConnections[i]; if (!row.id) { dataSourcesErrors[i] = { code: WorkspaceFormErrorCode.InvalidDataSource, @@ -347,7 +347,9 @@ export const validateWorkspaceForm = ( defaultMessage: 'Invalid data source', }), }; - } else if (isSelectedDataSourcesDuplicated(selectedDataSources.slice(0, i), row)) { + } else if ( + isSelectedDataSourceConnectionsDuplicated(selectedDataSourceConnections.slice(0, i), row) + ) { dataSourcesErrors[i] = { code: WorkspaceFormErrorCode.DuplicateDataSource, message: i18n.translate('workspace.form.permission.invalidate.group', { @@ -357,7 +359,7 @@ export const validateWorkspaceForm = ( } } if (Object.keys(dataSourcesErrors).length > 0) { - formErrors.selectedDataSources = dataSourcesErrors; + formErrors.selectedDataSourceConnections = dataSourcesErrors; } } return formErrors; @@ -447,6 +449,34 @@ const isSamePermissionSetting = (a: PermissionSettingLike, b: PermissionSettingL ); }; +export const isWorkspacePermissionSetting = ( + permissionSetting: PermissionSettingLike +): permissionSetting is WorkspacePermissionSetting => { + const { modes, type, userId, group } = permissionSetting; + if (!modes) { + return false; + } + const arrayStringify = (array: string[]) => array.sort().join(); + const stringifyModes = arrayStringify(modes); + if ( + Object.values(optionIdToWorkspacePermissionModesMap).every( + (validModes) => arrayStringify([...validModes]) !== stringifyModes + ) + ) { + return false; + } + if (type !== WorkspacePermissionItemType.User && type !== WorkspacePermissionItemType.Group) { + return false; + } + if (type === WorkspacePermissionItemType.User && !userId) { + return false; + } + if (type === WorkspacePermissionItemType.Group && !group) { + return false; + } + return true; +}; + export const getNumberOfChanges = ( newFormData: Partial, initialFormData: Partial diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_form_context.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_form_context.tsx index 417921f170d..9ac317f1ec0 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form_context.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form_context.tsx @@ -5,16 +5,17 @@ import React, { createContext, useContext, FormEventHandler, ReactNode } from 'react'; import { EuiColorPickerOutput } from '@elastic/eui/src/components/color_picker/color_picker'; -import { DataSource } from '../../../common/types'; -import { WorkspaceFormProps, WorkspaceFormErrors, WorkspacePermissionSetting } from './types'; +import { DataSourceConnection } from '../../../common/types'; +import { WorkspaceFormProps, WorkspaceFormErrors } from './types'; import { PublicAppInfo } from '../../../../../core/public'; import { useWorkspaceForm } from './use_workspace_form'; +import { WorkspaceFormDataState } from '../workspace_form'; interface WorkspaceFormContextProps { formId: string; setName: React.Dispatch>; setDescription: React.Dispatch>; - formData: any; + formData: WorkspaceFormDataState; isEditing: boolean; formErrors: WorkspaceFormErrors; setIsEditing: React.Dispatch>; @@ -26,11 +27,9 @@ interface WorkspaceFormContextProps { handleColorChange: (text: string, output: EuiColorPickerOutput) => void; handleUseCaseChange: (newUseCase: string) => void; setPermissionSettings: React.Dispatch< - React.SetStateAction< - Array & Partial> - > + React.SetStateAction >; - setSelectedDataSources: React.Dispatch>; + setSelectedDataSourceConnections: React.Dispatch>; } const initialContextValue: WorkspaceFormContextProps = {} as WorkspaceFormContextProps; From b4b1ac73936f656603bc537e53194e9f09e5c792 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 5 Sep 2024 21:40:19 +0800 Subject: [PATCH 21/21] Load direct query connections after data source tab selected Signed-off-by: Lin Wang --- .../select_data_source_panel.test.tsx | 14 ++++ .../select_data_source_panel.tsx | 11 ++- .../workspace_detail.test.tsx | 38 +++++++++- .../workspace_detail/workspace_detail.tsx | 37 +++++++++- .../components/workspace_detail_app.tsx | 20 ++---- src/plugins/workspace/public/utils.ts | 69 +++++++++++++------ 6 files changed, 149 insertions(+), 40 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.test.tsx b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.test.tsx index 2b2bef93f8d..1c15eac2119 100644 --- a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.test.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.test.tsx @@ -410,4 +410,18 @@ describe('SelectDataSourceDetailPanel', () => { expect(queryByText('Data Source 2')).toBeNull(); }); }); + + it('should show loading message when loading', async () => { + const { queryByText, getByText, rerender } = render( + selectDataSourceDetailPanel({ loading: false }) + ); + await waitFor(() => { + expect(queryByText('Loading data sources...')).not.toBeInTheDocument(); + }); + + rerender(selectDataSourceDetailPanel({ loading: true })); + await waitFor(() => { + expect(getByText('Loading data sources...')).toBeInTheDocument(); + }); + }); }); diff --git a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx index 95f463d9f87..7a503265983 100644 --- a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from 'react-intl'; +import { useUpdateEffect } from 'react-use'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { WorkspaceClient } from '../../workspace_client'; import { WorkspaceDetailConnectionTable } from './workspace_detail_connection_table'; @@ -59,6 +60,7 @@ export interface SelectDataSourceDetailPanelProps { isDashboardAdmin: boolean; currentWorkspace: WorkspaceObject; chrome: ChromeStart; + loading?: boolean; } export const SelectDataSourceDetailPanel = ({ @@ -67,12 +69,13 @@ export const SelectDataSourceDetailPanel = ({ isDashboardAdmin, currentWorkspace, chrome, + loading = false, }: SelectDataSourceDetailPanelProps) => { const { services: { notifications, workspaceClient, http }, } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); const { formData, setSelectedDataSourceConnections } = useWorkspaceFormContext(); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(loading); const [isVisible, setIsVisible] = useState(false); const [toggleIdSelected, setToggleIdSelected] = useState(toggleButtons[0].id); @@ -207,7 +210,7 @@ export const SelectDataSourceDetailPanel = ({ @@ -266,6 +269,10 @@ export const SelectDataSourceDetailPanel = ({ ); }; + useUpdateEffect(() => { + setIsLoading(loading); + }, [loading]); + return ( diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx index 0a528f433b8..dffc12da3bb 100644 --- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx @@ -3,17 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; +import { MemoryRouter } from 'react-router-dom'; import { PublicAppInfo, WorkspaceObject } from 'opensearch-dashboards/public'; import { coreMock } from '../../../../../core/public/mocks'; import { createOpenSearchDashboardsReactContext } from '../../../../opensearch_dashboards_react/public'; import { createMockedRegisteredUseCases$ } from '../../mocks'; import { WorkspaceDetail } from './workspace_detail'; import { WorkspaceFormProvider, WorkspaceOperationType } from '../workspace_form'; -import { MemoryRouter } from 'react-router-dom'; import { DataSourceConnectionType } from '../../../common/types'; +import * as utilsExports from '../../utils'; // all applications const PublicAPPInfoMap = new Map([ @@ -202,6 +203,9 @@ describe('WorkspaceDetail', () => { const { getByText } = render(WorkspaceDetailPage({ workspacesService: workspaceService })); fireEvent.click(getByText('Data sources')); expect(document.querySelector('#dataSources')).toHaveClass('euiTab-isSelected'); + await waitFor(() => { + expect(getByText('Loading data sources...')).toBeInTheDocument(); + }); }); it('delete button will been shown at page header', async () => { @@ -299,4 +303,34 @@ describe('WorkspaceDetail', () => { expect(alertSpy).toBeCalledTimes(0); alertSpy.mockRestore(); }); + + it('should show loaded data sources', async () => { + jest.spyOn(utilsExports, 'fetchDataSourceConnectionsByDataSourceIds').mockResolvedValue([ + { + id: 'dqc-1', + name: 'dqc-1-title', + description: 'dqc-1-description', + type: 'Amazon S3', + parentId: 'ds-1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + }, + { + id: 'dqc-2', + name: 'dqc-1-title', + description: 'dqc-1-description', + type: 'Amazon S3', + parentId: 'ds-1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + }, + ]); + const workspaceService = createWorkspacesSetupContractMockWithValue(workspaceObject); + const { getByText, getByRole } = render( + WorkspaceDetailPage({ workspacesService: workspaceService }) + ); + fireEvent.click(getByText('Data sources')); + await waitFor(() => { + expect(getByText('ds-1-title')).toBeInTheDocument(); + expect(getByRole('button', { name: '2' })).toBeInTheDocument(); + }); + }); }); diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx index 6a7d6955266..16f6c9c2fcf 100644 --- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx @@ -24,7 +24,12 @@ import { WORKSPACE_LIST_APP_ID } from '../../../common/constants'; import { cleanWorkspaceId } from '../../../../../core/public/utils'; import { DetailTab, DetailTabTitles, WorkspaceOperationType } from '../workspace_form/constants'; import { CoreStart, WorkspaceAttribute } from '../../../../../core/public'; -import { getFirstUseCaseOfFeatureConfigs, getUseCaseUrl } from '../../utils'; +import { + fetchDataSourceConnectionsByDataSourceIds, + fulfillRelatedConnections, + getFirstUseCaseOfFeatureConfigs, + getUseCaseUrl, +} from '../../utils'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { DataSourceManagementPluginSetup } from '../../../../../plugins/data_source_management/public'; import { SelectDataSourceDetailPanel } from './select_data_source_panel'; @@ -50,6 +55,7 @@ export const WorkspaceDetail = (props: WorkspaceDetailProps) => { uiSettings, navigationUI: { HeaderControl }, chrome, + notifications, }, } = useOpenSearchDashboards<{ CoreStart: CoreStart; @@ -64,11 +70,14 @@ export const WorkspaceDetail = (props: WorkspaceDetailProps) => { handleResetForm, numberOfChanges, setIsEditing, + formData, + setSelectedDataSourceConnections, } = useWorkspaceFormContext(); const [deletedWorkspace, setDeletedWorkspace] = useState(null); const [selectedTabId, setSelectedTabId] = useState(DetailTab.Details); const [modalVisible, setModalVisible] = useState(false); const [tabId, setTabId] = useState(DetailTab.Details); + const [isDQCFilled, setIsDQCFilled] = useState(false); const availableUseCases = useObservable(props.registeredUseCases$, []); const isDashboardAdmin = !!application?.capabilities?.dashboards?.isDashboardAdmin; @@ -88,6 +97,31 @@ export const WorkspaceDetail = (props: WorkspaceDetailProps) => { } }, [location.search]); + useEffect(() => { + if (selectedTabId !== DetailTab.DataSources || isDQCFilled || !http || !notifications) { + return; + } + fetchDataSourceConnectionsByDataSourceIds( + formData.selectedDataSourceConnections.map(({ id }) => id), + http + ) + .then((directQueryConnections) => { + setSelectedDataSourceConnections( + fulfillRelatedConnections(formData.selectedDataSourceConnections, directQueryConnections) + ); + }) + .finally(() => { + setIsDQCFilled(true); + }); + }, [ + http, + isDQCFilled, + selectedTabId, + notifications, + setSelectedDataSourceConnections, + formData.selectedDataSourceConnections, + ]); + if (!currentWorkspace || !application || !http || !savedObjects || !uiSettings || !chrome) { return null; } @@ -140,6 +174,7 @@ export const WorkspaceDetail = (props: WorkspaceDetailProps) => { name: DetailTabTitles.dataSources, content: ( { const rawFormData = getFormDataFromWorkspace(currentWorkspace); if (rawFormData && savedObjects && currentWorkspace) { - getDataSourcesList(savedObjects.client, [currentWorkspace.id]) - .then((assignedDataSources) => - fetchDataSourceConnections(assignedDataSources, http, notifications) - ) - .then((selectedDataSourceConnections) => { - setCurrentWorkspaceFormData({ - ...rawFormData, - // Connection with parent id means DQC, should filter them out - selectedDataSourceConnections: selectedDataSourceConnections.filter( - ({ parentId }) => !parentId - ), - }); + getDataSourcesList(savedObjects.client, [currentWorkspace.id]).then((dataSources) => { + setCurrentWorkspaceFormData({ + ...rawFormData, + // Direct query connections info is not required for all tabs, it can be fetched later + selectedDataSourceConnections: mergeDataSourcesWithConnections(dataSources, []), }); + }); } }, [currentWorkspace, savedObjects, http, notifications]); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 2620b470367..8a137a5c173 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -266,28 +266,46 @@ export const getDirectQueryConnections = async (dataSourceId: string, http: Http return directQueryConnections; }; -// Helper function to merge data sources with direct query connections -export const mergeDataSourcesWithConnections = ( - assignedDataSources: DataSource[], - directQueryConnections: DataSourceConnection[] -): DataSourceConnection[] => { - const dataSources: DataSourceConnection[] = []; - assignedDataSources.forEach((ds) => { - const relatedConnections = directQueryConnections.filter( - (directQueryConnection) => directQueryConnection.parentId === ds.id - ); - - dataSources.push({ +export const convertDataSourcesToOpenSearchConnections = ( + dataSources: DataSource[] +): DataSourceConnection[] => + dataSources.map((ds) => { + return { id: ds.id, type: ds.dataSourceEngineType, connectionType: DataSourceConnectionType.OpenSearchConnection, name: ds.title, description: ds.description, + relatedConnections: [], + }; + }); + +export const fulfillRelatedConnections = ( + connections: DataSourceConnection[], + directQueryConnections: DataSourceConnection[] +) => { + return connections.map((connection) => { + const relatedConnections = directQueryConnections.filter( + (directQueryConnection) => directQueryConnection.parentId === connection.id + ); + return { + ...connection, relatedConnections, - }); + }; }); +}; - return [...dataSources, ...directQueryConnections]; +// Helper function to merge data sources with direct query connections +export const mergeDataSourcesWithConnections = ( + dataSources: DataSource[], + directQueryConnections: DataSourceConnection[] +): DataSourceConnection[] => { + const openSearchConnections = convertDataSourcesToOpenSearchConnections(dataSources); + + return [ + ...fulfillRelatedConnections(openSearchConnections, directQueryConnections), + ...directQueryConnections, + ].sort((a, b) => a.name.localeCompare(b.name)); }; // If all connected data sources are serverless, will only allow to select essential use case. @@ -474,21 +492,28 @@ export const getUseCaseUrl = ( return useCaseURL; }; +export const fetchDataSourceConnectionsByDataSourceIds = async ( + dataSourceIds: string[], + http: HttpSetup | undefined +) => { + const directQueryConnectionsPromises = dataSourceIds.map((dataSourceId) => + getDirectQueryConnections(dataSourceId, http!).catch(() => []) + ); + const directQueryConnectionsResult = await Promise.all(directQueryConnectionsPromises); + return directQueryConnectionsResult.flat(); +}; + export const fetchDataSourceConnections = async ( assignedDataSources: DataSource[], http: HttpSetup | undefined, notifications: NotificationsStart | undefined ) => { try { - const directQueryConnectionsPromises = assignedDataSources.map((ds) => - getDirectQueryConnections(ds.id, http!).catch(() => []) + const directQueryConnections = await fetchDataSourceConnectionsByDataSourceIds( + assignedDataSources.map((ds) => ds.id), + http ); - const directQueryConnectionsResult = await Promise.all(directQueryConnectionsPromises); - const directQueryConnections = directQueryConnectionsResult.flat(); - return mergeDataSourcesWithConnections( - assignedDataSources, - directQueryConnections - ).sort((a, b) => a.name.localeCompare(b.name)); + return mergeDataSourcesWithConnections(assignedDataSources, directQueryConnections); } catch (error) { notifications?.toasts.addDanger( i18n.translate('workspace.detail.dataSources.error.message', {