diff --git a/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.tsx b/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.tsx index 50859803f8e0..88fbe57c7b9c 100644 --- a/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.tsx @@ -77,7 +77,7 @@ export interface AssociationDataSourceModalProps { savedObjects: SavedObjectsStart; assignedConnections: DataSourceConnection[]; closeModal: () => void; - handleAssignDataSourceConnections: (connections: DataSourceConnection[]) => Promise; + handleAssignDataSourceConnections: (connections: DataSourceConnection[]) => Promise | void; } export const AssociationDataSourceModal = ({ diff --git a/src/plugins/workspace/public/components/workspace_detail/opensearch_connections_table.tsx b/src/plugins/workspace/public/components/workspace_detail/opensearch_connections_table.tsx index 6083f557fc8e..2f9a32cf5a75 100644 --- a/src/plugins/workspace/public/components/workspace_detail/opensearch_connections_table.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/opensearch_connections_table.tsx @@ -32,9 +32,11 @@ import { AssociationDataSourceModalTab } from '../../../common/constants'; interface OpenSearchConnectionTableProps { isDashboardAdmin: boolean; - connectionType: string; + connectionType?: string; dataSourceConnections: DataSourceConnection[]; - handleUnassignDataSources: (dataSources: DataSourceConnection[]) => Promise; + inCreatePage?: boolean; + handleUnassignDataSources: (dataSources: DataSourceConnection[]) => Promise | void; + getSelectedItems?: (dataSources: DataSourceConnection[]) => void; } export const OpenSearchConnectionTable = ({ @@ -42,6 +44,8 @@ export const OpenSearchConnectionTable = ({ connectionType, dataSourceConnections, handleUnassignDataSources, + getSelectedItems, + inCreatePage = false, }: OpenSearchConnectionTableProps) => { const [selectedItems, setSelectedItems] = useState([]); const [modalVisible, setModalVisible] = useState(false); @@ -50,6 +54,12 @@ export const OpenSearchConnectionTable = ({ Record >({}); + useEffect(() => { + if (inCreatePage && getSelectedItems) { + getSelectedItems(selectedItems); + } + }, [selectedItems, getSelectedItems, inCreatePage]); + useEffect(() => { // Reset selected items when connectionType changes setSelectedItems([]); @@ -162,7 +172,7 @@ export const OpenSearchConnectionTable = ({ width: '25%', field: 'name', name: i18n.translate('workspace.detail.dataSources.table.title', { - defaultMessage: 'Title', + defaultMessage: 'Data source', }), truncateText: true, render: (name: string, record) => { @@ -274,7 +284,11 @@ export const OpenSearchConnectionTable = ({ type: 'icon', onClick: (item: DataSourceConnection) => { setSelectedItems([item]); - setModalVisible(true); + if (inCreatePage) { + handleUnassignDataSources([item]); + } else { + setModalVisible(true); + } }, 'data-test-subj': 'workspace-detail-dataSources-table-actions-remove', }, @@ -291,23 +305,36 @@ export const OpenSearchConnectionTable = ({ return ( <> - + {inCreatePage ? ( + + ) : ( + + )} - {modalVisible && ( + {modalVisible && !inCreatePage && ( ({ - getDataSourcesList: jest.fn().mockResolvedValue(dataSources), -})); +const dataSources = [ + { + id: 'id3', + title: 'title3', + description: 'ds-3-description', + auth: '', + dataSourceEngineType: '', + workspaces: [], + }, + { + id: 'id4', + title: 'title4', + description: 'ds-4-description', + auth: '', + dataSourceEngineType: '', + workspaces: [], + }, +]; + +jest.spyOn(utils, 'getDataSourcesList').mockResolvedValue(dataSources); 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 }); + const { getByText } = setup({ assignedDataSources: currentAssignedDataSources }); - expect(getByText(dataSources[0].title)).toBeInTheDocument(); - expect(getByText(dataSources[1].title)).toBeInTheDocument(); + expect(getByText(currentAssignedDataSources[0].name)).toBeInTheDocument(); + expect(getByText(currentAssignedDataSources[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(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 { getByTestId } = setup({ onChange: onChangeMock, - selectedDataSources: [{ id: '', title: '' }], + assignedDataSources: [{ id: '', name: '', type: '', connectionType: 0 }], }); expect(onChangeMock).not.toHaveBeenCalled(); await act(() => { - fireEvent.click(getByLabelText('Delete data source')); + fireEvent.click(getByTestId('workspace-detail-dataSources-table-actions-remove')); }); 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 9c990d7e7195..03b2dc645cf7 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,132 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { - EuiSmallButton, - EuiCompressedFormRow, EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, - EuiCompressedComboBox, - EuiComboBoxOptionOption, EuiFormLabel, + EuiText, + EuiFlexItem, + EuiSmallButton, + EuiFlexGroup, } 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 } from '../../../../../core/public'; +import { DataSourceConnection } from '../../../common/types'; import { WorkspaceFormError } from './types'; +import { AssociationDataSourceModal } from '../workspace_detail/association_data_source_modal'; +import { OpenSearchConnectionTable } from '../workspace_detail/opensearch_connections_table'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { WorkspaceClient } from '../../workspace_client'; +import { AssociationDataSourceModalTab } from '../../../common/constants'; export interface SelectDataSourcePanelProps { errors?: { [key: number]: WorkspaceFormError }; savedObjects: SavedObjectsStart; - selectedDataSources: DataSource[]; - onChange: (value: DataSource[]) => void; + assignedDataSources: DataSourceConnection[]; + onChange: (value: DataSourceConnection[]) => void; + isDashboardAdmin: boolean; } export const SelectDataSourcePanel = ({ errors, onChange, - selectedDataSources, + assignedDataSources, savedObjects, + isDashboardAdmin, }: SelectDataSourcePanelProps) => { - const [dataSourcesOptions, setDataSourcesOptions] = useState([]); - useEffect(() => { - if (!savedObjects) return; - getDataSourcesList(savedObjects.client, ['*']).then((result) => { - const options = result.map(({ title, id }) => ({ - label: title, - value: id, - })); - setDataSourcesOptions(options); - }); - }, [savedObjects, setDataSourcesOptions]); - const handleAddNewOne = useCallback(() => { - onChange?.([ - ...selectedDataSources, - { - title: '', - id: '', - }, - ]); - }, [onChange, selectedDataSources]); + const [modalVisible, setModalVisible] = useState(false); + const [selectedItems, setSelectedItems] = useState([]); + const { + services: { notifications, http }, + } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); - 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 handleAssignDataSources = (dataSources: DataSourceConnection[]) => { + setModalVisible(false); + const savedDataSources: DataSourceConnection[] = [...assignedDataSources, ...dataSources]; + onChange(savedDataSources); + }; - onChange(newSelectedOptions); - }, - [onChange, selectedDataSources] - ); + const handleUnassignDataSources = (dataSources: DataSourceConnection[]) => { + const savedDataSources = (assignedDataSources ?? [])?.filter( + ({ id }: DataSourceConnection) => !dataSources.some((item) => item.id === id) + ); + onChange(savedDataSources); + }; + + const renderTableContent = () => { + return ( + + ); + }; - const handleDelete = useCallback( - (index) => { - const newSelectedOptions = [...selectedDataSources]; - newSelectedOptions.splice(index, 1); + const associationButton = ( + setModalVisible(true)} + data-test-subj="workspace-creator-dataSources-assign-button" + > + {i18n.translate('workspace.form.selectDataSourcePanel.addNew', { + defaultMessage: 'Add data sources', + })} + + ); - 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', - })} + + {i18n.translate('workspace.form.selectDataSource.subTitle', { + defaultMessage: 'Add data sources that will be available in the workspace', + })} + - - {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 && {associationButton}} + + + + {assignedDataSources.length > 0 && renderTableContent()} + + {modalVisible && ( + setModalVisible(false)} + handleAssignDataSourceConnections={handleAssignDataSources} + http={http} + 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 5ec929e17b0c..a9a7df9005d4 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 { DetailTab, 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[]; + selectedDataSources?: DataSourceConnection[]; } export interface WorkspaceFormData extends WorkspaceFormSubmitData { 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 f627df1f3aad..87b64376de8f 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 @@ -12,7 +12,7 @@ import { getUseCaseFeatureConfig, isUseCaseFeatureConfig, } from '../../utils'; -import { DataSource } from '../../../common/types'; +import { DataSourceConnection } from '../../../common/types'; import { WorkspaceFormProps, WorkspaceFormErrors, WorkspacePermissionSetting } from './types'; import { generatePermissionSettingsState, @@ -49,7 +49,7 @@ export const useWorkspaceForm = ({ Array & Partial> >(initialPermissionSettingsRef.current); - const [selectedDataSources, setSelectedDataSources] = useState( + const [selectedDataSources, setSelectedDataSources] = useState( defaultValues?.selectedDataSources && defaultValues.selectedDataSources.length > 0 ? defaultValues.selectedDataSources : [] diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx index f21a800a8357..17ab4bb37d9a 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -48,7 +48,7 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { const disabledUserOrGroupInputIdsRef = useRef( defaultValues?.permissionSettings?.map((item) => item.id) ?? [] ); - const isDashboardAdmin = application?.capabilities?.dashboards?.isDashboardAdmin ?? false; + const isDashboardAdmin = !!application?.capabilities?.dashboards?.isDashboardAdmin; const handleNameInputChange = useCallback( (newName) => { setName(newName); @@ -140,8 +140,9 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { errors={formErrors.selectedDataSources} onChange={setSelectedDataSources} savedObjects={savedObjects} - selectedDataSources={formData.selectedDataSources} + assignedDataSources={formData.selectedDataSources} data-test-subj={`workspaceForm-dataSourcePanel`} + isDashboardAdmin={isDashboardAdmin} /> )}