From 38b004a81ac962bf864ef0a67b5eb336c25c4252 Mon Sep 17 00:00:00 2001 From: yubonluo Date: Fri, 23 Aug 2024 10:57:36 +0800 Subject: [PATCH 01/24] add dqc Signed-off-by: yubonluo --- .../opensearch_connections_table.tsx | 66 +++++++++++-------- .../select_data_source_panel.tsx | 52 +++++++++++++-- 2 files changed, 87 insertions(+), 31 deletions(-) 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 ed6ce46965ac..401c6c018e92 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 @@ -45,6 +45,43 @@ export const OpenSearchConnectionTable = ({ const [assignItems, setAssignItems] = useState([]); const [modalVisible, setModalVisible] = useState(false); + const renderToolsLeft = () => { + 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 }, + })} + , + ] + : []; + }; + + const search = { + toolsLeft: renderToolsLeft(), + box: { + schema: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'dataSourceEngineType', + name: 'Type', + multiSelect: 'or', + options: assignedDataSources.map((ds) => ({ + value: ds.dataSourceEngineType, + name: ds.dataSourceEngineType, + view: `${ds.dataSourceEngineType}`, + })), + }, + ], + }; + const filteredDataSources = useMemo( () => assignedDataSources.filter((dataSource) => @@ -159,38 +196,13 @@ export const OpenSearchConnectionTable = ({ }; 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 }, - })} - - - )} - - - - - { + setToggleIdSelected(optionId); + }; const handleAssignDataSources = async (dataSources: DataSource[]) => { try { @@ -90,7 +118,7 @@ export const SelectDataSourceDetailPanel = ({ data-test-subj="workspace-detail-dataSources-assign-button" > {i18n.translate('workspace.detail.dataSources.assign.button', { - defaultMessage: 'Association OpenSearch Connections', + defaultMessage: 'Associate data sources', })} ); @@ -110,11 +138,13 @@ export const SelectDataSourceDetailPanel = ({ const noAssociationMessage = ( + +

@@ -122,7 +152,7 @@ export const SelectDataSourceDetailPanel = ({ {isDashboardAdmin ? ( @@ -166,8 +196,22 @@ export const SelectDataSourceDetailPanel = ({

{detailTitle}

- {isDashboardAdmin && {associationButton}} + + + + onChange(id)} + isFullWidth + /> + + {isDashboardAdmin && {associationButton}} + + + {renderTableContent()} {isVisible && ( From 689e0f68e6565b1c320bd4830e67a29d709a717d Mon Sep 17 00:00:00 2001 From: yubonluo Date: Mon, 26 Aug 2024 14:52:45 +0800 Subject: [PATCH 02/24] add dircet query connections on the detail page Signed-off-by: yubonluo --- .../data_source_management/public/index.ts | 3 +- src/plugins/workspace/common/types.ts | 24 +++ .../workspace/opensearch_dashboards.json | 2 +- .../public/assets/prometheus_logo.svg | 9 + .../workspace/public/assets/s3_logo.svg | 9 + .../opensearch_connections_table.tsx | 166 +++++++++++++----- .../select_data_source_panel.tsx | 39 +++- .../components/workspace_form/constants.ts | 2 +- src/plugins/workspace/public/utils.ts | 45 +++++ 9 files changed, 248 insertions(+), 51 deletions(-) create mode 100644 src/plugins/workspace/public/assets/prometheus_logo.svg create mode 100644 src/plugins/workspace/public/assets/s3_logo.svg diff --git a/src/plugins/data_source_management/public/index.ts b/src/plugins/data_source_management/public/index.ts index 35a0474947d2..9bb1a9cc8b42 100644 --- a/src/plugins/data_source_management/public/index.ts +++ b/src/plugins/data_source_management/public/index.ts @@ -11,7 +11,7 @@ export function plugin() { return new DataSourceManagementPlugin(); } -export { DataSourceManagementPluginStart } from './types'; +export { DataSourceManagementPluginStart, DirectQueryDatasourceDetails } from './types'; export { DataSourceSelector, DataSourceOption } from './components/data_source_selector'; export { DataSourceMenu } from './components/data_source_menu'; export { DataSourceManagementPlugin, DataSourceManagementPluginSetup } from './plugin'; @@ -26,3 +26,4 @@ export { } from './components/data_source_menu'; export { DataSourceSelectionService } from './service/data_source_selection_service'; export { getDefaultDataSourceId, getDefaultDataSourceId$ } from './components/utils'; +export { DATACONNECTIONS_BASE } from './constants'; diff --git a/src/plugins/workspace/common/types.ts b/src/plugins/workspace/common/types.ts index cf621a09143e..0484770ed465 100644 --- a/src/plugins/workspace/common/types.ts +++ b/src/plugins/workspace/common/types.ts @@ -4,6 +4,7 @@ */ import { DataSourceAttributes } from 'src/plugins/data_source/common/data_sources'; +import { DirectQueryDatasourceDetails } from 'src/plugins/data_source_management/public'; export type DataSource = Pick< DataSourceAttributes, @@ -12,3 +13,26 @@ export type DataSource = Pick< // Id defined in SavedObjectAttribute could be single or array, here only should be single string. id: string; }; + +export type DirectQueryConnection = Pick< + DirectQueryDatasourceDetails, + 'name' | 'connector' | 'status' | 'description' +> & { + // DQC parent data source id + parentId: string; +}; + +export enum DataSourceConnectionType { + OpenSearchConnection, + DirectQueryConnection, +} + +export interface DataSourceConnection { + id: string; + type: string | undefined; + parentId?: string; + connectionType: DataSourceConnectionType; + name: string; + description?: string; + relatedConnections?: DataSourceConnection[]; +} diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json index ac24c20d712b..4bde3b6016dc 100644 --- a/src/plugins/workspace/opensearch_dashboards.json +++ b/src/plugins/workspace/opensearch_dashboards.json @@ -9,5 +9,5 @@ "navigation" ], "optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement","contentManagement"], - "requiredBundles": ["opensearchDashboardsReact", "home","dataSource"] + "requiredBundles": ["opensearchDashboardsReact", "home","dataSource", "dataSourceManagement"] } diff --git a/src/plugins/workspace/public/assets/prometheus_logo.svg b/src/plugins/workspace/public/assets/prometheus_logo.svg new file mode 100644 index 000000000000..e21c6a7e2859 --- /dev/null +++ b/src/plugins/workspace/public/assets/prometheus_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/plugins/workspace/public/assets/s3_logo.svg b/src/plugins/workspace/public/assets/s3_logo.svg new file mode 100644 index 000000000000..5b0c3a35aaad --- /dev/null +++ b/src/plugins/workspace/public/assets/s3_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + 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 130c47741d16..43a75ee5cc79 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 @@ -3,51 +3,75 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiSpacer, - EuiButton, - EuiFlexItem, - EuiFlexGroup, - EuiFieldSearch, EuiInMemoryTable, EuiBasicTableColumn, EuiTableSelectionType, EuiTableActionsColumnType, EuiConfirmModal, + EuiSearchBarProps, + EuiText, + EuiListGroup, + EuiListGroupItem, + EuiIcon, + EuiPopover, + EuiButtonEmpty, + EuiPopoverTitle, + EuiSmallButton, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { CoreStart, WorkspaceObject } from '../../../../../core/public'; -import { DataSource } from '../../../common/types'; +import { DataSource, DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { WorkspaceClient } from '../../workspace_client'; import { convertPermissionSettingsToPermissions, useWorkspaceFormContext } from '../workspace_form'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import PrometheusLogo from '../../assets/prometheus_logo.svg'; +import S3Logo from '../../assets/s3_logo.svg'; interface OpenSearchConnectionTableProps { - assignedDataSources: DataSource[]; isDashboardAdmin: boolean; currentWorkspace: WorkspaceObject; setIsLoading: React.Dispatch>; + connectionType: string; + dataSourceConnections: DataSourceConnection[]; } export const OpenSearchConnectionTable = ({ - assignedDataSources, isDashboardAdmin, currentWorkspace, setIsLoading, + connectionType, + dataSourceConnections, }: OpenSearchConnectionTableProps) => { const { services: { notifications, workspaceClient }, } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); const { formData, setSelectedDataSources } = useWorkspaceFormContext(); - const [searchTerm, setSearchTerm] = useState(''); - const [selectedItems, setSelectedItems] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); const [modalVisible, setModalVisible] = useState(false); + const [popoversState, setPopoversState] = useState>({}); - const renderToolsLeft = () => { + const filteredDataSources = useMemo(() => { + // Reset the item when switching connectionType. + setSelectedItems([]); + if (connectionType === 'openSearchConnections') { + return dataSourceConnections.filter( + (dqc) => dqc.connectionType === DataSourceConnectionType.OpenSearchConnection + ); + } else if (connectionType === 'directQueryConnections') { + return dataSourceConnections.filter( + (dqc) => dqc.connectionType === DataSourceConnectionType.DirectQueryConnection + ); + } + return dataSourceConnections; + }, [connectionType, dataSourceConnections]); + + const renderToolsLeft = useCallback(() => { return selectedItems.length > 0 && !modalVisible ? [ - setModalVisible(true)} data-test-subj="workspace-detail-dataSources-table-bulkRemove" @@ -56,44 +80,37 @@ export const OpenSearchConnectionTable = ({ defaultMessage: 'Remove {numberOfSelect} association(s)', values: { numberOfSelect: selectedItems.length }, })} - , + , ] : []; + }, [selectedItems, modalVisible]); + + const onSelectionChange = (selectedDataSources: DataSourceConnection[]) => { + setSelectedItems(selectedDataSources); }; - const search = { + const search: EuiSearchBarProps = { toolsLeft: renderToolsLeft(), box: { - schema: true, + incremental: true, }, filters: [ { type: 'field_value_selection', - field: 'dataSourceEngineType', + field: 'type', name: 'Type', multiSelect: 'or', - options: assignedDataSources.map((ds) => ({ - value: ds.dataSourceEngineType, - name: ds.dataSourceEngineType, - view: `${ds.dataSourceEngineType}`, + options: Array.from( + new Set(filteredDataSources.map(({ type }) => type).filter(Boolean)) + ).map((type) => ({ + value: type!, + name: type!, })), }, ], }; - const filteredDataSources = useMemo( - () => - assignedDataSources.filter((dataSource) => - dataSource.title.toLowerCase().includes(searchTerm.toLowerCase()) - ), - [searchTerm, assignedDataSources] - ); - - const onSelectionChange = (selectedDataSources: DataSource[]) => { - setSelectedItems(selectedDataSources); - }; - - const handleUnassignDataSources = async (dataSources: DataSource[]) => { + const handleUnassignDataSources = async (dataSources: DataSourceConnection[]) => { try { setIsLoading(true); setModalVisible(false); @@ -130,16 +147,33 @@ export const OpenSearchConnectionTable = ({ } }; - const columns: Array> = [ + const directQueryConnectionIcon = (connector: string | undefined) => { + switch (connector) { + case 'S3GLUE': + return ; + case 'PROMETHEUS': + return ; + default: + return <>; + } + }; + const togglePopover = (itemId: string) => { + setPopoversState((prevState) => ({ + ...prevState, + [itemId]: !prevState[itemId], + })); + }; + + const columns: Array> = [ { - field: 'title', + field: 'name', name: i18n.translate('workspace.detail.dataSources.table.title', { defaultMessage: 'Title', }), truncateText: true, }, { - field: 'dataSourceEngineType', + field: 'type', name: i18n.translate('workspace.detail.dataSources.table.type', { defaultMessage: 'Type', }), @@ -152,6 +186,54 @@ export const OpenSearchConnectionTable = ({ }), truncateText: true, }, + { + field: 'relatedConnections', + name: i18n.translate('workspace.detail.dataSources.table.relatedConnections', { + defaultMessage: 'Related connections', + }), + align: 'right', + truncateText: true, + render: (relatedConnections: DataSourceConnection[], record) => + relatedConnections?.length > 0 ? ( + togglePopover(record.id)} + button={ + togglePopover(record.id)} + > + {relatedConnections?.length} + + } + > + RELATED CONNECTIONS + + {relatedConnections.map((item) => ( + + ))} + + + ) : ( + + ), + }, ...(isDashboardAdmin ? [ { @@ -172,7 +254,7 @@ export const OpenSearchConnectionTable = ({ ), icon: 'unlink', type: 'icon', - onClick: (item: DataSource) => { + onClick: (item: DataSourceConnection) => { setSelectedItems([item]); setModalVisible(true); }, @@ -184,14 +266,11 @@ export const OpenSearchConnectionTable = ({ : []), ]; - const selection: EuiTableSelectionType = { + const selection: EuiTableSelectionType = { selectable: () => isDashboardAdmin, onSelectionChange, }; - const handleSearch = (e: React.ChangeEvent) => { - setSearchTerm(e.target.value); - }; return ( <> { setModalVisible(false); @@ -224,7 +304,7 @@ export const OpenSearchConnectionTable = ({ defaultMessage: 'Cancel', })} confirmButtonText={i18n.translate('workspace.detail.dataSources.Modal.confirmButton', { - defaultMessage: 'Remove connections', + defaultMessage: 'Remove data source(s)', })} buttonColor="danger" defaultFocusedButton="confirm" 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 8d8b7e558bc2..7575e252cacb 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, { useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiText, EuiTitle, @@ -20,13 +20,14 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from 'react-intl'; -import { DataSource } from '../../../common/types'; +import { DataSource, DataSourceConnection } from '../../../common/types'; import { WorkspaceClient } from '../../workspace_client'; import { OpenSearchConnectionTable } from './opensearch_connections_table'; import { AssociationDataSourceModal } from './association_data_source_modal'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { CoreStart, SavedObjectsStart, WorkspaceObject } from '../../../../../core/public'; import { convertPermissionSettingsToPermissions, useWorkspaceFormContext } from '../workspace_form'; +import { getDirectQueryConnections, mergeDataSourcesWithConnections } from '../../utils'; export interface SelectDataSourcePanelProps { savedObjects: SavedObjectsStart; @@ -44,13 +45,40 @@ export const SelectDataSourceDetailPanel = ({ currentWorkspace, }: SelectDataSourcePanelProps) => { const { - services: { notifications, workspaceClient }, + services: { notifications, workspaceClient, http }, } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); const { formData, setSelectedDataSources } = useWorkspaceFormContext(); const [isLoading, setIsLoading] = useState(false); const [isVisible, setIsVisible] = useState(false); + const [dataSourceConnections, setDataSourceConnections] = useState([]); const [toggleIdSelected, setToggleIdSelected] = useState('all'); + const fetchDQC = useCallback(async () => { + setIsLoading(true); + try { + const directQueryConnectionsPromises = assignedDataSources.map((ds) => + getDirectQueryConnections(ds.id, http!) + ); + const directQueryConnectionsResult = await Promise.all(directQueryConnectionsPromises); + const directQueryConnections = directQueryConnectionsResult.flat(); + setDataSourceConnections( + mergeDataSourcesWithConnections(assignedDataSources, directQueryConnections) + ); + } catch (error) { + notifications?.toasts.addDanger( + i18n.translate('workspace.detail.dataSources.error.message', { + defaultMessage: 'Can not fetch direct query connections', + }) + ); + } finally { + setIsLoading(false); + } + }, [assignedDataSources, http, notifications?.toasts]); + + useEffect(() => { + fetchDQC(); + }, [fetchDQC]); + const toggleButtons = [ { id: 'all', @@ -130,7 +158,7 @@ export const SelectDataSourceDetailPanel = ({ @@ -182,8 +210,9 @@ export const SelectDataSourceDetailPanel = ({ ); }; diff --git a/src/plugins/workspace/public/components/workspace_form/constants.ts b/src/plugins/workspace/public/components/workspace_form/constants.ts index 1224d5abb0ec..ed2268bec8d7 100644 --- a/src/plugins/workspace/public/components/workspace_form/constants.ts +++ b/src/plugins/workspace/public/components/workspace_form/constants.ts @@ -117,7 +117,7 @@ export const DetailTabTitles: { [key in DetailTab]: string } = { defaultMessage: 'Details', }), [DetailTab.DataSources]: i18n.translate('workspace.detail.tabTitle.dataSources', { - defaultMessage: 'Data Sources', + defaultMessage: 'Data sources', }), [DetailTab.Collaborators]: i18n.translate('workspace.detail.tabTitle.collaborators', { defaultMessage: 'Collaborators', diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 2bc4f7a80155..e48a0682ad74 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -27,6 +27,11 @@ import { DEFAULT_SELECTED_FEATURES_IDS, WORKSPACE_DETAIL_APP_ID } from '../commo import { WorkspaceUseCase } from './types'; import { formatUrlWithWorkspaceId } from '../../../core/public/utils'; import { SigV4ServiceName } from '../../../plugins/data_source/common/data_sources'; +import { + DirectQueryDatasourceDetails, + DATACONNECTIONS_BASE, +} from '../../data_source_management/public'; +import { DataSource, DataSourceConnection, DataSourceConnectionType } from '../common/types'; export const USE_CASE_PREFIX = 'use-case-'; @@ -233,6 +238,46 @@ export const getDataSourcesList = (client: SavedObjectsStart['client'], workspac }); }; +export const getDirectQueryConnections = async (dataSourceId: string, http: HttpSetup) => { + const endpoint = `${DATACONNECTIONS_BASE}/dataSourceMDSId=${dataSourceId}`; + const res = await http.get(endpoint); + const directQueryConnections: DataSourceConnection[] = res.map( + (dataConnection: DirectQueryDatasourceDetails) => ({ + id: `${dataSourceId}-${dataConnection.name}`, + name: dataConnection.name, + type: dataConnection.connector, + connectionType: DataSourceConnectionType.DirectQueryConnection, + description: dataConnection.description, + parentId: dataSourceId, + }) + ); + 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({ + id: ds.id, + type: ds.dataSourceEngineType, + connectionType: DataSourceConnectionType.OpenSearchConnection, + name: ds.title, + description: ds.description, + relatedConnections, + }); + }); + + return [...dataSources, ...directQueryConnections]; +}; + // If all connected data sources are serverless, will only allow to select essential use case. export const getIsOnlyAllowEssentialUseCase = async (client: SavedObjectsStart['client']) => { const allDataSources = await getDataSourcesList(client, ['*']); From 13d5ba8cc1afd8fd3d339e1fa0c13e8cc4d24a97 Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 06:55:42 +0000 Subject: [PATCH 03/24] Changeset file for PR #7839 created/updated --- changelogs/fragments/7839.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/7839.yml diff --git a/changelogs/fragments/7839.yml b/changelogs/fragments/7839.yml new file mode 100644 index 000000000000..3ba2fdc290e2 --- /dev/null +++ b/changelogs/fragments/7839.yml @@ -0,0 +1,2 @@ +fix: +- Enable direct query connections to support in workspace ([#7839](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7839)) \ No newline at end of file From c8141e30dd89654b1aecff60459c84756aac0331 Mon Sep 17 00:00:00 2001 From: yubonluo Date: Mon, 26 Aug 2024 15:00:56 +0800 Subject: [PATCH 04/24] delete useless code Signed-off-by: yubonluo --- src/plugins/workspace/common/types.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/plugins/workspace/common/types.ts b/src/plugins/workspace/common/types.ts index 0484770ed465..4b64f62d9e29 100644 --- a/src/plugins/workspace/common/types.ts +++ b/src/plugins/workspace/common/types.ts @@ -4,7 +4,6 @@ */ import { DataSourceAttributes } from 'src/plugins/data_source/common/data_sources'; -import { DirectQueryDatasourceDetails } from 'src/plugins/data_source_management/public'; export type DataSource = Pick< DataSourceAttributes, @@ -14,14 +13,6 @@ export type DataSource = Pick< id: string; }; -export type DirectQueryConnection = Pick< - DirectQueryDatasourceDetails, - 'name' | 'connector' | 'status' | 'description' -> & { - // DQC parent data source id - parentId: string; -}; - export enum DataSourceConnectionType { OpenSearchConnection, DirectQueryConnection, From 0e6ad2f88072e9129f651d62deaad5c6d41b23cf Mon Sep 17 00:00:00 2001 From: yubonluo Date: Mon, 26 Aug 2024 16:15:18 +0800 Subject: [PATCH 05/24] optimize the code Signed-off-by: yubonluo --- .../opensearch_connections_table.tsx | 69 +++++-------------- .../select_data_source_panel.tsx | 39 ++++++++++- 2 files changed, 56 insertions(+), 52 deletions(-) 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 43a75ee5cc79..8f3fb6e4431a 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 @@ -20,35 +20,26 @@ import { EuiButtonEmpty, EuiPopoverTitle, EuiSmallButton, + EuiLink, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import { CoreStart, WorkspaceObject } from '../../../../../core/public'; -import { DataSource, DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; -import { WorkspaceClient } from '../../workspace_client'; -import { convertPermissionSettingsToPermissions, useWorkspaceFormContext } from '../workspace_form'; -import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import PrometheusLogo from '../../assets/prometheus_logo.svg'; import S3Logo from '../../assets/s3_logo.svg'; interface OpenSearchConnectionTableProps { isDashboardAdmin: boolean; - currentWorkspace: WorkspaceObject; - setIsLoading: React.Dispatch>; connectionType: string; dataSourceConnections: DataSourceConnection[]; + handleUnassignDataSources: (dataSources: DataSourceConnection[]) => Promise; } export const OpenSearchConnectionTable = ({ isDashboardAdmin, - currentWorkspace, - setIsLoading, connectionType, dataSourceConnections, + handleUnassignDataSources, }: OpenSearchConnectionTableProps) => { - const { - services: { notifications, workspaceClient }, - } = useOpenSearchDashboards<{ CoreStart: CoreStart; workspaceClient: WorkspaceClient }>(); - const { formData, setSelectedDataSources } = useWorkspaceFormContext(); const [selectedItems, setSelectedItems] = useState([]); const [modalVisible, setModalVisible] = useState(false); const [popoversState, setPopoversState] = useState>({}); @@ -110,43 +101,6 @@ export const OpenSearchConnectionTable = ({ ], }; - const handleUnassignDataSources = async (dataSources: DataSourceConnection[]) => { - try { - setIsLoading(true); - setModalVisible(false); - const { permissionSettings, selectedDataSources, useCase, ...attributes } = formData; - const savedDataSources = (selectedDataSources ?? [])?.filter( - ({ id }: DataSource) => !dataSources.some((item) => item.id === id) - ); - - const result = await workspaceClient.update(currentWorkspace.id, attributes, { - dataSources: savedDataSources.map(({ id }: DataSource) => id), - permissions: convertPermissionSettingsToPermissions(permissionSettings), - }); - if (result?.success) { - notifications?.toasts.addSuccess({ - title: i18n.translate('workspace.detail.dataSources.unassign.success', { - defaultMessage: 'Remove associated OpenSearch connections successfully', - }), - }); - setSelectedDataSources(savedDataSources); - } else { - throw new Error( - result?.error ? result?.error : 'Remove associated OpenSearch connections failed' - ); - } - } catch (error) { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.detail.dataSources.unassign.failed', { - defaultMessage: 'Failed to remove associated OpenSearch connections', - }), - text: error instanceof Error ? error.message : JSON.stringify(error), - }); - } finally { - setIsLoading(false); - } - }; - const directQueryConnectionIcon = (connector: string | undefined) => { switch (connector) { case 'S3GLUE': @@ -171,6 +125,20 @@ export const OpenSearchConnectionTable = ({ defaultMessage: 'Title', }), truncateText: true, + render: (name: string, record) => { + const origin = window.location.origin; + let url: string; + if (record.connectionType === DataSourceConnectionType.OpenSearchConnection) { + url = `${origin}/app/dataSources_core/${record.id}`; + } else { + url = `${origin}/app/dataSources_core/manage/${name}?dataSourceMDSId=${record.parentId}`; + } + return ( + + {name} + + ); + }, }, { field: 'type', @@ -298,6 +266,7 @@ export const OpenSearchConnectionTable = ({ setSelectedItems([]); }} onConfirm={() => { + setModalVisible(false); handleUnassignDataSources(selectedItems); }} cancelButtonText={i18n.translate('workspace.detail.dataSources.modal.cancelButton', { 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 7575e252cacb..ae6741b36668 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 @@ -139,6 +139,42 @@ export const SelectDataSourceDetailPanel = ({ } }; + const handleUnassignDataSources = async (dataSources: DataSourceConnection[]) => { + try { + setIsLoading(true); + const { permissionSettings, selectedDataSources, useCase, ...attributes } = formData; + const savedDataSources = (selectedDataSources ?? [])?.filter( + ({ id }: DataSource) => !dataSources.some((item) => item.id === id) + ); + + const result = await workspaceClient.update(currentWorkspace.id, attributes, { + dataSources: savedDataSources.map(({ id }: DataSource) => id), + permissions: convertPermissionSettingsToPermissions(permissionSettings), + }); + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.detail.dataSources.unassign.success', { + defaultMessage: 'Remove associated OpenSearch connections successfully', + }), + }); + setSelectedDataSources(savedDataSources); + } else { + throw new Error( + result?.error ? result?.error : 'Remove associated OpenSearch connections failed' + ); + } + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.detail.dataSources.unassign.failed', { + defaultMessage: 'Failed to remove associated OpenSearch connections', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + } finally { + setIsLoading(false); + } + }; + const associationButton = ( setIsVisible(true)} @@ -209,10 +245,9 @@ export const SelectDataSourceDetailPanel = ({ return ( ); }; From 0ef40b604a7d39b87391d2582cf315fcc1258b12 Mon Sep 17 00:00:00 2001 From: yubonluo Date: Mon, 26 Aug 2024 17:19:29 +0800 Subject: [PATCH 06/24] optimize the code Signed-off-by: yubonluo --- .../opensearch_connections_table.tsx | 13 ++- .../select_data_source_panel.tsx | 100 +++++++----------- src/plugins/workspace/public/hooks.ts | 38 ++++++- 3 files changed, 88 insertions(+), 63 deletions(-) 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 8f3fb6e4431a..c1d0989991ac 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 @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiSpacer, EuiInMemoryTable, @@ -44,6 +44,11 @@ export const OpenSearchConnectionTable = ({ const [modalVisible, setModalVisible] = useState(false); const [popoversState, setPopoversState] = useState>({}); + useEffect(() => { + // Reset selected items when connectionType changes + setSelectedItems([]); + }, [connectionType, setSelectedItems]); + const filteredDataSources = useMemo(() => { // Reset the item when switching connectionType. setSelectedItems([]); @@ -180,7 +185,11 @@ export const OpenSearchConnectionTable = ({ } > - RELATED CONNECTIONS + + {i18n.translate('workspace.detail.dataSources.recentConnections.title', { + defaultMessage: 'RELATED CONNECTIONS', + })} + ([]); const [toggleIdSelected, setToggleIdSelected] = useState('all'); + const fetchDQC = useFetchDQC(assignedDataSources, http, notifications); - const fetchDQC = useCallback(async () => { + useEffect(() => { setIsLoading(true); - try { - const directQueryConnectionsPromises = assignedDataSources.map((ds) => - getDirectQueryConnections(ds.id, http!) - ); - const directQueryConnectionsResult = await Promise.all(directQueryConnectionsPromises); - const directQueryConnections = directQueryConnectionsResult.flat(); - setDataSourceConnections( - mergeDataSourcesWithConnections(assignedDataSources, directQueryConnections) - ); - } catch (error) { - notifications?.toasts.addDanger( - i18n.translate('workspace.detail.dataSources.error.message', { - defaultMessage: 'Can not fetch direct query connections', - }) - ); - } finally { + fetchDQC().then((res) => { + setDataSourceConnections(res); setIsLoading(false); - } - }, [assignedDataSources, http, notifications?.toasts]); - - useEffect(() => { - fetchDQC(); + }); }, [fetchDQC]); const toggleButtons = [ @@ -100,10 +83,6 @@ export const SelectDataSourceDetailPanel = ({ }, ]; - const onChange = (optionId: string) => { - setToggleIdSelected(optionId); - }; - const handleAssignDataSources = async (dataSources: DataSource[]) => { try { setIsLoading(true); @@ -139,41 +118,44 @@ export const SelectDataSourceDetailPanel = ({ } }; - const handleUnassignDataSources = async (dataSources: DataSourceConnection[]) => { - try { - setIsLoading(true); - const { permissionSettings, selectedDataSources, useCase, ...attributes } = formData; - const savedDataSources = (selectedDataSources ?? [])?.filter( - ({ id }: DataSource) => !dataSources.some((item) => item.id === id) - ); + const handleUnassignDataSources = useCallback( + async (dataSources: DataSourceConnection[]) => { + try { + setIsLoading(true); + const { permissionSettings, selectedDataSources, useCase, ...attributes } = formData; + const savedDataSources = (selectedDataSources ?? [])?.filter( + ({ id }: DataSource) => !dataSources.some((item) => item.id === id) + ); - const result = await workspaceClient.update(currentWorkspace.id, attributes, { - dataSources: savedDataSources.map(({ id }: DataSource) => id), - permissions: convertPermissionSettingsToPermissions(permissionSettings), - }); - if (result?.success) { - notifications?.toasts.addSuccess({ - title: i18n.translate('workspace.detail.dataSources.unassign.success', { - defaultMessage: 'Remove associated OpenSearch connections successfully', + const result = await workspaceClient.update(currentWorkspace.id, attributes, { + dataSources: savedDataSources.map(({ id }: DataSource) => id), + permissions: convertPermissionSettingsToPermissions(permissionSettings), + }); + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.detail.dataSources.unassign.success', { + defaultMessage: 'Remove associated OpenSearch connections successfully', + }), + }); + setSelectedDataSources(savedDataSources); + } else { + throw new Error( + result?.error ? result?.error : 'Remove associated OpenSearch connections failed' + ); + } + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.detail.dataSources.unassign.failed', { + defaultMessage: 'Failed to remove associated OpenSearch connections', }), + text: error instanceof Error ? error.message : JSON.stringify(error), }); - setSelectedDataSources(savedDataSources); - } else { - throw new Error( - result?.error ? result?.error : 'Remove associated OpenSearch connections failed' - ); + } finally { + setIsLoading(false); } - } catch (error) { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.detail.dataSources.unassign.failed', { - defaultMessage: 'Failed to remove associated OpenSearch connections', - }), - text: error instanceof Error ? error.message : JSON.stringify(error), - }); - } finally { - setIsLoading(false); - } - }; + }, + [currentWorkspace.id, formData, notifications?.toasts, setSelectedDataSources, workspaceClient] + ); const associationButton = ( onChange(id)} + onChange={(id) => setToggleIdSelected(id)} isFullWidth /> diff --git a/src/plugins/workspace/public/hooks.ts b/src/plugins/workspace/public/hooks.ts index 875e9b494f23..93a2e4a38e79 100644 --- a/src/plugins/workspace/public/hooks.ts +++ b/src/plugins/workspace/public/hooks.ts @@ -3,9 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ApplicationStart, PublicAppInfo } from 'opensearch-dashboards/public'; +import { + ApplicationStart, + HttpSetup, + NotificationsStart, + PublicAppInfo, +} from 'opensearch-dashboards/public'; import { useObservable } from 'react-use'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; +import { i18n } from '@osd/i18n'; +import { getDirectQueryConnections, mergeDataSourcesWithConnections } from './utils'; +import { DataSource } from '../common/types'; export function useApplications(applicationInstance: ApplicationStart) { const applications = useObservable(applicationInstance.applications$); @@ -17,3 +25,29 @@ export function useApplications(applicationInstance: ApplicationStart) { return apps; }, [applications]); } + +export const useFetchDQC = ( + assignedDataSources: DataSource[], + http: HttpSetup | undefined, + notifications: NotificationsStart | undefined +) => { + const fetchDQC = useCallback(async () => { + try { + const directQueryConnectionsPromises = assignedDataSources.map((ds) => + getDirectQueryConnections(ds.id, http!) + ); + const directQueryConnectionsResult = await Promise.all(directQueryConnectionsPromises); + const directQueryConnections = directQueryConnectionsResult.flat(); + return mergeDataSourcesWithConnections(assignedDataSources, directQueryConnections); + } catch (error) { + notifications?.toasts.addDanger( + i18n.translate('workspace.detail.dataSources.error.message', { + defaultMessage: 'Cannot fetch direct query connections', + }) + ); + return []; + } + }, [assignedDataSources, http, notifications?.toasts]); + + return fetchDQC; +}; From 810651ccb24c53b593d1daac9df63c62d3189ad2 Mon Sep 17 00:00:00 2001 From: Kapian1234 Date: Tue, 27 Aug 2024 12:02:37 +0800 Subject: [PATCH 07/24] Refactor association modal Signed-off-by: Kapian1234 --- .../association_data_source_modal.tsx | 200 +++++++++++++++--- 1 file changed, 175 insertions(+), 25 deletions(-) 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 999ac80f7ad0..2b2558f5ac73 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 @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Fragment, useEffect, useMemo, useState } from 'react'; +import { Fragment, useEffect, useMemo, useState, useCallback } from 'react'; import React from 'react'; import { EuiText, @@ -15,17 +15,43 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiSelectableOption, + EuiSpacer, + EuiButtonGroup, + EuiButtonGroupOptionProps, + EuiBadge, } from '@elastic/eui'; import { FormattedMessage } from 'react-intl'; +import { i18n } from '@osd/i18n'; import { getDataSourcesList } from '../../utils'; -import { DataSource } from '../../../common/types'; +import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { SavedObjectsStart } from '../../../../../core/public'; +const tabOptions: EuiButtonGroupOptionProps[] = [ + { + id: 'all', + label: i18n.translate('workspace.form.selectDataSource.subTitle', { + defaultMessage: 'All', + }), + }, + { + id: 'opensearch-connections', + label: i18n.translate('workspace.form.selectDataSource.subTitle', { + defaultMessage: 'OpenSearch connections', + }), + }, + { + id: 'direct-query-connections', + label: i18n.translate('workspace.form.selectDataSource.subTitle', { + defaultMessage: 'Direct query connections', + }), + }, +]; + export interface AssociationDataSourceModalProps { savedObjects: SavedObjectsStart; - assignedDataSources: DataSource[]; + assignedDataSources: DataSourceConnection[]; closeModal: () => void; - handleAssignDataSources: (dataSources: DataSource[]) => Promise; + handleAssignDataSources: (dataSources: DataSourceConnection[]) => Promise; } export const AssociationDataSourceModal = ({ @@ -34,60 +60,184 @@ export const AssociationDataSourceModal = ({ assignedDataSources, handleAssignDataSources, }: AssociationDataSourceModalProps) => { - const [options, setOptions] = useState([]); - const [allDataSources, setAllDataSources] = useState([]); + // const [options, setOptions] = useState>>([]); + // const [allOptions, setAlloptions] = useState([]); + const [allDataSources, setAllDataSources] = useState([]); + const [currentTab, setCurrentTab] = useState('all'); + const [allOptions, setAllOptions] = useState>>([]); useEffect(() => { getDataSourcesList(savedObjects.client, ['*']).then((result) => { - const filteredDataSources = result.filter( - ({ id }: DataSource) => !assignedDataSources.some((ds) => ds.id === id) - ); + const filteredDataSources: DataSourceConnection[] = result + .filter(({ id }) => !assignedDataSources.some((ds) => ds.id === id)) + .map((datasource) => { + return { + ...datasource, + name: datasource.title, + type: datasource.dataSourceEngineType, + connectionType: DataSourceConnectionType.OpenSearchConnection, + }; + }); + + // dqc + filteredDataSources.push({ + name: 's3 connection1', + description: 'this is a s3', + id: filteredDataSources[0].id + '-' + 's3 connection1', + parentId: filteredDataSources[0].id, + type: 's3', + connectionType: DataSourceConnectionType.DirectQueryConnection, + }); + filteredDataSources.push({ + name: 's3 connection2', + description: 'this is a s3', + id: filteredDataSources[0].id + '-' + 's3 connection2', + parentId: filteredDataSources[0].id, + type: 's3', + connectionType: DataSourceConnectionType.DirectQueryConnection, + }); + filteredDataSources[0].relatedConnections = [ + filteredDataSources[filteredDataSources.length - 1], + filteredDataSources[filteredDataSources.length - 2], + ]; + setAllDataSources(filteredDataSources); - setOptions( + setAllOptions( filteredDataSources.map((dataSource) => ({ - label: dataSource.title, + ...dataSource, + label: dataSource.name, key: dataSource.id, + append: dataSource.relatedConnections ? ( + + {i18n.translate('workspace.form.selectDataSource.optionBadge', { + defaultMessage: '+' + dataSource.relatedConnections.length + ' related', + })} + + ) : undefined, })) ); }); }, [assignedDataSources, savedObjects]); const selectedDataSources = useMemo(() => { - const selectedIds = options + const selectedIds = allOptions .filter((option: EuiSelectableOption) => option.checked) .map((option: EuiSelectableOption) => option.key); return allDataSources.filter((ds) => selectedIds.includes(ds.id)); - }, [options, allDataSources]); + }, [allOptions, allDataSources]); + + const options = useMemo(() => { + if (currentTab === 'all') { + return allOptions; + } + if (currentTab === 'opensearch-connections') { + return allOptions.filter( + (dataSource) => dataSource.connectionType === DataSourceConnectionType.OpenSearchConnection + ); + } + if (currentTab === 'direct-query-connections') { + return allOptions.filter( + (dataSource) => dataSource.connectionType === DataSourceConnectionType.DirectQueryConnection + ); + } + }, [allOptions, currentTab]); + + const handleSelectionChange = useCallback( + (newOptions: Array>) => { + const newCheckedOptionIds = newOptions + .filter(({ checked }) => checked === 'on') + .map(({ value }) => value); + + setAllOptions((prevOptions) => { + return prevOptions.map((option) => { + const checkedInNewOptions = newCheckedOptionIds.includes(option.value); + const connection = allDataSources.find(({ id }) => id === option.value); + option.checked = checkedInNewOptions ? 'on' : undefined; + + if (!connection) { + return option; + } + + if (connection.type === 'DS') { + const childDQCIds = allDataSources + .find(({ parentId }) => parentId === connection.id) + ?.relatedConnections?.map(({ id }) => id); + // Check if there any DQC change to checked status this time, set to "on" if exists. + if ( + newCheckedOptionIds.some( + (id) => + childDQCIds.includes(id) && + // This child DQC not checked before + !prevOptions.find(({ value, checked }) => value === id && checked === 'on') + ) + ) { + option.checked = 'on'; + } + } + + if (connection.type === 'DQC') { + const parentConnection = allDataSources.find(({ id }) => id === connection.id); + if (parentConnection) { + const isParentCheckedLastTime = prevOptions.find( + ({ value, checked }) => value === parentConnection.id && checked === 'on' + ); + const isParentCheckedThisTime = newCheckedOptionIds.includes(parentConnection.id); + + // Parent change to checked this time + if (!isParentCheckedLastTime && isParentCheckedThisTime) { + option.checked = 'on'; + } + + if (isParentCheckedLastTime && isParentCheckedThisTime) { + option.checked = undefined; + } + } + } + + return option; + }); + }); + }, + [allDataSources] + ); return ( - + -

- -

+
- + + + setCurrentTab(id)} + buttonSize="compressed" + // isFullWidth={true} + /> + setOptions(newOptions)} + onChange={handleSelectionChange} > {(list, search) => ( @@ -99,7 +249,7 @@ export const AssociationDataSourceModal = ({ - + From 2409005b7094eabd62137ed3321baacfef3f10e9 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 27 Aug 2024 16:15:24 +0800 Subject: [PATCH 08/24] Integrate modal with workspace detail page Signed-off-by: Lin Wang --- .../association_data_source_modal.tsx | 206 +++++++++--------- .../select_data_source_panel.tsx | 39 +++- src/plugins/workspace/public/hooks.ts | 26 --- src/plugins/workspace/public/utils.ts | 25 ++- 4 files changed, 152 insertions(+), 144 deletions(-) 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 2b2558f5ac73..037eaeae8d2c 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 @@ -22,25 +22,60 @@ import { } from '@elastic/eui'; import { FormattedMessage } from 'react-intl'; import { i18n } from '@osd/i18n'; -import { getDataSourcesList } from '../../utils'; + +import { getDataSourcesList, fetchDataSourceConnections } from '../../utils'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; -import { SavedObjectsStart } from '../../../../../core/public'; +import { HttpStart, NotificationsStart, SavedObjectsStart } from '../../../../../core/public'; + +type DataSourceModalOption = EuiSelectableOption<{ connection: DataSourceConnection }>; + +const convertConnectionsToOptions = ( + connections: DataSourceConnection[], + assignedConnections: DataSourceConnection[] +) => { + const assignedConnectionIds = assignedConnections.map(({ id }) => id); + return connections + .filter((connection) => !assignedConnectionIds.includes(connection.id)) + .map((connection) => ({ + label: connection.name, + key: connection.id, + append: + connection.relatedConnections && connection.relatedConnections.length > 0 ? ( + + {i18n.translate('workspace.form.selectDataSource.optionBadge', { + defaultMessage: '+ {relatedConnections} related', + values: { + relatedConnections: connection.relatedConnections.length, + }, + })} + + ) : undefined, + connection, + checked: undefined, + })); +}; + +enum AssociationDataSourceModalTab { + All = 'all', + OpenSearchConnections = 'opensearch-connections', + DirectQueryConnections = 'direction-query-connections', +} const tabOptions: EuiButtonGroupOptionProps[] = [ { - id: 'all', + id: AssociationDataSourceModalTab.All, label: i18n.translate('workspace.form.selectDataSource.subTitle', { defaultMessage: 'All', }), }, { - id: 'opensearch-connections', + id: AssociationDataSourceModalTab.OpenSearchConnections, label: i18n.translate('workspace.form.selectDataSource.subTitle', { defaultMessage: 'OpenSearch connections', }), }, { - id: 'direct-query-connections', + id: AssociationDataSourceModalTab.DirectQueryConnections, label: i18n.translate('workspace.form.selectDataSource.subTitle', { defaultMessage: 'Direct query connections', }), @@ -48,147 +83,92 @@ const tabOptions: EuiButtonGroupOptionProps[] = [ ]; export interface AssociationDataSourceModalProps { + http: HttpStart | undefined; + notifications: NotificationsStart | undefined; savedObjects: SavedObjectsStart; - assignedDataSources: DataSourceConnection[]; + assignedConnections: DataSourceConnection[]; closeModal: () => void; - handleAssignDataSources: (dataSources: DataSourceConnection[]) => Promise; + handleAssignDataSourceConnections: (connections: DataSourceConnection[]) => Promise; } export const AssociationDataSourceModal = ({ + http, + notifications, closeModal, savedObjects, - assignedDataSources, - handleAssignDataSources, + assignedConnections, + handleAssignDataSourceConnections, }: AssociationDataSourceModalProps) => { - // const [options, setOptions] = useState>>([]); - // const [allOptions, setAlloptions] = useState([]); - const [allDataSources, setAllDataSources] = useState([]); + const [allConnections, setAllConnections] = useState([]); const [currentTab, setCurrentTab] = useState('all'); - const [allOptions, setAllOptions] = useState>>([]); - - useEffect(() => { - getDataSourcesList(savedObjects.client, ['*']).then((result) => { - const filteredDataSources: DataSourceConnection[] = result - .filter(({ id }) => !assignedDataSources.some((ds) => ds.id === id)) - .map((datasource) => { - return { - ...datasource, - name: datasource.title, - type: datasource.dataSourceEngineType, - connectionType: DataSourceConnectionType.OpenSearchConnection, - }; - }); - - // dqc - filteredDataSources.push({ - name: 's3 connection1', - description: 'this is a s3', - id: filteredDataSources[0].id + '-' + 's3 connection1', - parentId: filteredDataSources[0].id, - type: 's3', - connectionType: DataSourceConnectionType.DirectQueryConnection, - }); - filteredDataSources.push({ - name: 's3 connection2', - description: 'this is a s3', - id: filteredDataSources[0].id + '-' + 's3 connection2', - parentId: filteredDataSources[0].id, - type: 's3', - connectionType: DataSourceConnectionType.DirectQueryConnection, - }); - filteredDataSources[0].relatedConnections = [ - filteredDataSources[filteredDataSources.length - 1], - filteredDataSources[filteredDataSources.length - 2], - ]; - - setAllDataSources(filteredDataSources); - setAllOptions( - filteredDataSources.map((dataSource) => ({ - ...dataSource, - label: dataSource.name, - key: dataSource.id, - append: dataSource.relatedConnections ? ( - - {i18n.translate('workspace.form.selectDataSource.optionBadge', { - defaultMessage: '+' + dataSource.relatedConnections.length + ' related', - })} - - ) : undefined, - })) - ); - }); - }, [assignedDataSources, savedObjects]); - - const selectedDataSources = useMemo(() => { - const selectedIds = allOptions - .filter((option: EuiSelectableOption) => option.checked) - .map((option: EuiSelectableOption) => option.key); - - return allDataSources.filter((ds) => selectedIds.includes(ds.id)); - }, [allOptions, allDataSources]); + const [allOptions, setAllOptions] = useState([]); + const [isLoading, setIsLoading] = useState(false); const options = useMemo(() => { - if (currentTab === 'all') { - return allOptions; - } - if (currentTab === 'opensearch-connections') { + if (currentTab === AssociationDataSourceModalTab.OpenSearchConnections) { return allOptions.filter( - (dataSource) => dataSource.connectionType === DataSourceConnectionType.OpenSearchConnection + ({ connection }) => + connection.connectionType === DataSourceConnectionType.OpenSearchConnection ); } - if (currentTab === 'direct-query-connections') { + if (currentTab === AssociationDataSourceModalTab.DirectQueryConnections) { return allOptions.filter( - (dataSource) => dataSource.connectionType === DataSourceConnectionType.DirectQueryConnection + ({ connection }) => + connection.connectionType === DataSourceConnectionType.DirectQueryConnection ); } + return allOptions; }, [allOptions, currentTab]); + const selectedConnections = useMemo( + () => allOptions.filter(({ checked }) => checked === 'on').map(({ connection }) => connection), + [allOptions] + ); + const handleSelectionChange = useCallback( - (newOptions: Array>) => { - const newCheckedOptionIds = newOptions + (newOptions: DataSourceModalOption[]) => { + const newCheckedConnectionIds = newOptions .filter(({ checked }) => checked === 'on') - .map(({ value }) => value); + .map(({ connection }) => connection.id); setAllOptions((prevOptions) => { return prevOptions.map((option) => { - const checkedInNewOptions = newCheckedOptionIds.includes(option.value); - const connection = allDataSources.find(({ id }) => id === option.value); + option = { ...option }; + const checkedInNewOptions = newCheckedConnectionIds.includes(option.connection.id); + const connection = option.connection; option.checked = checkedInNewOptions ? 'on' : undefined; - if (!connection) { - return option; - } - - if (connection.type === 'DS') { - const childDQCIds = allDataSources - .find(({ parentId }) => parentId === connection.id) - ?.relatedConnections?.map(({ id }) => id); + if (connection.connectionType === DataSourceConnectionType.OpenSearchConnection) { + const childDQCIds = allConnections + .filter(({ parentId }) => parentId === connection.id) + .map(({ id }) => id); // Check if there any DQC change to checked status this time, set to "on" if exists. if ( - newCheckedOptionIds.some( + newCheckedConnectionIds.some( (id) => childDQCIds.includes(id) && // This child DQC not checked before - !prevOptions.find(({ value, checked }) => value === id && checked === 'on') + !prevOptions.find((item) => item.connection.id === id && item.checked === 'on') ) ) { option.checked = 'on'; } } - if (connection.type === 'DQC') { - const parentConnection = allDataSources.find(({ id }) => id === connection.id); + if (connection.connectionType === DataSourceConnectionType.DirectQueryConnection) { + const parentConnection = allConnections.find(({ id }) => id === connection.parentId); if (parentConnection) { - const isParentCheckedLastTime = prevOptions.find( - ({ value, checked }) => value === parentConnection.id && checked === 'on' + const isParentCheckedLastTime = !!prevOptions.find( + (item) => item.connection.id === parentConnection.id && item.checked === 'on' ); - const isParentCheckedThisTime = newCheckedOptionIds.includes(parentConnection.id); + const isParentCheckedThisTime = newCheckedConnectionIds.includes(parentConnection.id); // Parent change to checked this time if (!isParentCheckedLastTime && isParentCheckedThisTime) { option.checked = 'on'; } + // This won't be executed since checked options already been filter out if (isParentCheckedLastTime && isParentCheckedThisTime) { option.checked = undefined; } @@ -199,9 +179,25 @@ export const AssociationDataSourceModal = ({ }); }); }, - [allDataSources] + [allConnections] ); + useEffect(() => { + setIsLoading(true); + getDataSourcesList(savedObjects.client, ['*']) + .then((dataSourcesList) => fetchDataSourceConnections(dataSourcesList, http, notifications)) + .then((connections) => { + setAllConnections(connections); + }) + .finally(() => { + setIsLoading(false); + }); + }, [savedObjects.client, http, notifications]); + + useEffect(() => { + setAllOptions(convertConnectionsToOptions(allConnections, assignedConnections)); + }, [allConnections, assignedConnections]); + return ( @@ -226,7 +222,6 @@ export const AssociationDataSourceModal = ({ idSelected={currentTab} onChange={(id) => setCurrentTab(id)} buttonSize="compressed" - // isFullWidth={true} /> {(list, search) => ( @@ -256,8 +252,8 @@ export const AssociationDataSourceModal = ({ /> handleAssignDataSources(selectedDataSources)} - isDisabled={!selectedDataSources || selectedDataSources.length === 0} + onClick={() => handleAssignDataSourceConnections(selectedConnections)} + isDisabled={!selectedConnections || selectedConnections.length === 0} fill > ([]); + const [assignedDataSourceConnections, setAssignedDataSourceConnections] = useState< + DataSourceConnection[] + >([]); const [toggleIdSelected, setToggleIdSelected] = useState('all'); - const fetchDQC = useFetchDQC(assignedDataSources, http, notifications); useEffect(() => { setIsLoading(true); - fetchDQC().then((res) => { - setDataSourceConnections(res); + fetchDataSourceConnections(assignedDataSources, http, notifications).then((connections) => { + setAssignedDataSourceConnections(connections); setIsLoading(false); }); - }, [fetchDQC]); + }, [assignedDataSources, http, notifications]); const toggleButtons = [ { @@ -83,7 +84,19 @@ export const SelectDataSourceDetailPanel = ({ }, ]; - const handleAssignDataSources = async (dataSources: DataSource[]) => { + const handleAssignDataSourceConnections = async ( + dataSourceConnections: 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); @@ -228,7 +241,7 @@ export const SelectDataSourceDetailPanel = ({ ); @@ -262,10 +275,12 @@ export const SelectDataSourceDetailPanel = ({ {renderTableContent()} {isVisible && ( setIsVisible(false)} - handleAssignDataSources={handleAssignDataSources} + assignedConnections={assignedDataSourceConnections} + handleAssignDataSourceConnections={handleAssignDataSourceConnections} /> )} diff --git a/src/plugins/workspace/public/hooks.ts b/src/plugins/workspace/public/hooks.ts index 93a2e4a38e79..0e036fcd04ce 100644 --- a/src/plugins/workspace/public/hooks.ts +++ b/src/plugins/workspace/public/hooks.ts @@ -25,29 +25,3 @@ export function useApplications(applicationInstance: ApplicationStart) { return apps; }, [applications]); } - -export const useFetchDQC = ( - assignedDataSources: DataSource[], - http: HttpSetup | undefined, - notifications: NotificationsStart | undefined -) => { - const fetchDQC = useCallback(async () => { - try { - const directQueryConnectionsPromises = assignedDataSources.map((ds) => - getDirectQueryConnections(ds.id, http!) - ); - const directQueryConnectionsResult = await Promise.all(directQueryConnectionsPromises); - const directQueryConnections = directQueryConnectionsResult.flat(); - return mergeDataSourcesWithConnections(assignedDataSources, directQueryConnections); - } catch (error) { - notifications?.toasts.addDanger( - i18n.translate('workspace.detail.dataSources.error.message', { - defaultMessage: 'Cannot fetch direct query connections', - }) - ); - return []; - } - }, [assignedDataSources, http, notifications?.toasts]); - - return fetchDQC; -}; diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index e48a0682ad74..9061c3425e39 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -2,7 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - +import { i18n } from '@osd/i18n'; import { combineLatest } from 'rxjs'; import { NavGroupType, @@ -13,6 +13,7 @@ import { ChromeBreadcrumb, ApplicationStart, HttpSetup, + NotificationsStart, } from '../../../core/public'; import { App, @@ -436,3 +437,25 @@ export const getUseCaseUrl = ( ); return useCaseURL; }; + +export const fetchDataSourceConnections = async ( + assignedDataSources: DataSource[], + http: HttpSetup | undefined, + notifications: NotificationsStart | undefined +) => { + try { + const directQueryConnectionsPromises = assignedDataSources.map((ds) => + getDirectQueryConnections(ds.id, http!).catch(() => []) + ); + const directQueryConnectionsResult = await Promise.all(directQueryConnectionsPromises); + const directQueryConnections = directQueryConnectionsResult.flat(); + return mergeDataSourcesWithConnections(assignedDataSources, directQueryConnections); + } catch (error) { + notifications?.toasts.addDanger( + i18n.translate('workspace.detail.dataSources.error.message', { + defaultMessage: 'Cannot fetch direct query connections', + }) + ); + return []; + } +}; From 4f6d8d965e7a5e23bfdf518164f72d434ad4950d Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 27 Aug 2024 16:52:40 +0800 Subject: [PATCH 09/24] Fix parent data source unchecked Signed-off-by: Lin Wang --- .../association_data_source_modal.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) 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 037eaeae8d2c..3eec04d5ae53 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 @@ -163,14 +163,9 @@ export const AssociationDataSourceModal = ({ ); const isParentCheckedThisTime = newCheckedConnectionIds.includes(parentConnection.id); - // Parent change to checked this time - if (!isParentCheckedLastTime && isParentCheckedThisTime) { - option.checked = 'on'; - } - - // This won't be executed since checked options already been filter out - if (isParentCheckedLastTime && isParentCheckedThisTime) { - option.checked = undefined; + // Update checked status if parent checked status changed this time + if (isParentCheckedLastTime !== isParentCheckedThisTime) { + option.checked = isParentCheckedThisTime ? 'on' : undefined; } } } From f1a54af510b713fef1305f8b1c4e2c7affbedcc0 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 28 Aug 2024 11:23:55 +0800 Subject: [PATCH 10/24] Remove all tab and sort connections by name alphabetical Signed-off-by: Lin Wang --- .../workspace_detail/association_data_source_modal.tsx | 9 +-------- src/plugins/workspace/public/utils.ts | 5 ++++- 2 files changed, 5 insertions(+), 9 deletions(-) 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 3eec04d5ae53..74e9d47628e8 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 @@ -56,18 +56,11 @@ const convertConnectionsToOptions = ( }; enum AssociationDataSourceModalTab { - All = 'all', OpenSearchConnections = 'opensearch-connections', DirectQueryConnections = 'direction-query-connections', } const tabOptions: EuiButtonGroupOptionProps[] = [ - { - id: AssociationDataSourceModalTab.All, - label: i18n.translate('workspace.form.selectDataSource.subTitle', { - defaultMessage: 'All', - }), - }, { id: AssociationDataSourceModalTab.OpenSearchConnections, label: i18n.translate('workspace.form.selectDataSource.subTitle', { @@ -100,7 +93,7 @@ export const AssociationDataSourceModal = ({ handleAssignDataSourceConnections, }: AssociationDataSourceModalProps) => { const [allConnections, setAllConnections] = useState([]); - const [currentTab, setCurrentTab] = useState('all'); + const [currentTab, setCurrentTab] = useState(tabOptions[0].id); const [allOptions, setAllOptions] = useState([]); const [isLoading, setIsLoading] = useState(false); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 9061c3425e39..5dc78b044e8c 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -449,7 +449,10 @@ export const fetchDataSourceConnections = async ( ); const directQueryConnectionsResult = await Promise.all(directQueryConnectionsPromises); const directQueryConnections = directQueryConnectionsResult.flat(); - return mergeDataSourcesWithConnections(assignedDataSources, directQueryConnections); + return mergeDataSourcesWithConnections( + assignedDataSources, + directQueryConnections + ).sort((a, b) => a.name.localeCompare(b.name)); } catch (error) { notifications?.toasts.addDanger( i18n.translate('workspace.detail.dataSources.error.message', { From 2a252b97799ed09e86a01069cfb68dea4226bf9c Mon Sep 17 00:00:00 2001 From: yubonluo Date: Wed, 28 Aug 2024 13:55:27 +0800 Subject: [PATCH 11/24] optimzie Signed-off-by: yubonluo --- .../public/components/workspace_detail/a.scss | 16 +++ .../opensearch_connections_table.tsx | 128 ++++++++++++++++-- .../select_data_source_panel.tsx | 8 +- src/plugins/workspace/public/hooks.ts | 12 +- 4 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_detail/a.scss diff --git a/src/plugins/workspace/public/components/workspace_detail/a.scss b/src/plugins/workspace/public/components/workspace_detail/a.scss new file mode 100644 index 000000000000..8286a8ad6d8e --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_detail/a.scss @@ -0,0 +1,16 @@ +.hide-header .euiTableHeader { + display: none; + } + +.no-padding-table { + padding: 0 !important; /* 消除所有内边距 */ + margin: 0 !important; /* 消除所有外边距,视需求而定 */ +} + +.no-padding-table .euiTableCellContent { + padding: 0 !important; /* 消除单元格内边距 */ +} + +.noTableHeader thead { + display: none; + } 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 c1d0989991ac..6e458f994301 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 @@ -21,6 +21,10 @@ import { EuiPopoverTitle, EuiSmallButton, EuiLink, + EuiSmallButtonIcon, + EuiFlexItem, + EuiFlexGroup, + EuiBasicTable, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; @@ -43,26 +47,22 @@ export const OpenSearchConnectionTable = ({ const [selectedItems, setSelectedItems] = useState([]); const [modalVisible, setModalVisible] = useState(false); const [popoversState, setPopoversState] = useState>({}); + // const [selectedItems, setSelectedItems] = useState([]); + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< + Record + >({}); useEffect(() => { // Reset selected items when connectionType changes setSelectedItems([]); + setItemIdToExpandedRowMap({}); }, [connectionType, setSelectedItems]); const filteredDataSources = useMemo(() => { - // Reset the item when switching connectionType. - setSelectedItems([]); - if (connectionType === 'openSearchConnections') { - return dataSourceConnections.filter( - (dqc) => dqc.connectionType === DataSourceConnectionType.OpenSearchConnection - ); - } else if (connectionType === 'directQueryConnections') { - return dataSourceConnections.filter( - (dqc) => dqc.connectionType === DataSourceConnectionType.DirectQueryConnection - ); - } - return dataSourceConnections; - }, [connectionType, dataSourceConnections]); + return dataSourceConnections.filter( + (dqc) => dqc.connectionType === DataSourceConnectionType.OpenSearchConnection + ); + }, [dataSourceConnections]); const renderToolsLeft = useCallback(() => { return selectedItems.length > 0 && !modalVisible @@ -123,7 +123,107 @@ export const OpenSearchConnectionTable = ({ })); }; + const toggleDetails = (item: DataSourceConnection) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMapValues[item.id]) { + delete itemIdToExpandedRowMapValues[item.id]; + } else { + const { relatedConnections } = item; + // itemIdToExpandedRowMapValues[item.id] = ( + // + // {relatedConnections?.map((relatedConnection) => ( + // + // {relatedConnection.name} + // {relatedConnection.type} + // {relatedConnection.description} + // - + // + // } + // /> + // ))} + // + // ); + itemIdToExpandedRowMapValues[item.id] = ( + + ); + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; + + const columns1: Array> = [ + { + field: 'name', + name: i18n.translate('workspace.detail.dataSources.table.title', { + defaultMessage: 'Title', + }), + truncateText: true, + render: (name: string, record) => { + const origin = window.location.origin; + let url: string; + if (record.connectionType === DataSourceConnectionType.OpenSearchConnection) { + url = `${origin}/app/dataSources_core/${record.id}`; + } else { + url = `${origin}/app/dataSources_core/manage/${name}?dataSourceMDSId=${record.parentId}`; + } + return ( + + {name} + + ); + }, + }, + { + field: 'type', + name: i18n.translate('workspace.detail.dataSources.table.type', { + defaultMessage: 'Type', + }), + truncateText: true, + }, + { + field: 'description', + name: i18n.translate('workspace.detail.dataSources.table.description', { + defaultMessage: 'Description', + }), + truncateText: true, + }, + { + field: 'relatedConnections', + name: i18n.translate('workspace.detail.dataSources.table.relatedConnections', { + defaultMessage: 'Related connections', + }), + align: 'right', + truncateText: true, + render: (relatedConnections: DataSourceConnection[], record) => , + }, + ]; + const columns: Array> = [ + { + width: '40px', + isExpander: true, + render: (item: DataSourceConnection) => + connectionType === 'directQueryConnections' && item?.relatedConnections?.length ? ( + toggleDetails(item)} + aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} + iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + /> + ) : null, + }, { field: 'name', name: i18n.translate('workspace.detail.dataSources.table.title', { @@ -258,6 +358,8 @@ export const OpenSearchConnectionTable = ({ search={search} key={connectionType} isSelectable={true} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + isExpandable={true} pagination={{ initialPageSize: 10, pageSizeOptions: [10, 20, 30], 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 0eb67ba52dea..a0de9c8c03ba 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 @@ -53,7 +53,7 @@ export const SelectDataSourceDetailPanel = ({ const [assignedDataSourceConnections, setAssignedDataSourceConnections] = useState< DataSourceConnection[] >([]); - const [toggleIdSelected, setToggleIdSelected] = useState('all'); + const [toggleIdSelected, setToggleIdSelected] = useState('openSearchConnections'); useEffect(() => { setIsLoading(true); @@ -64,12 +64,6 @@ export const SelectDataSourceDetailPanel = ({ }, [assignedDataSources, http, notifications]); const toggleButtons = [ - { - id: 'all', - label: i18n.translate('workspace.detail.dataSources.all', { - defaultMessage: 'All', - }), - }, { id: 'openSearchConnections', label: i18n.translate('workspace.detail.dataSources.openSearchConnections', { diff --git a/src/plugins/workspace/public/hooks.ts b/src/plugins/workspace/public/hooks.ts index 0e036fcd04ce..875e9b494f23 100644 --- a/src/plugins/workspace/public/hooks.ts +++ b/src/plugins/workspace/public/hooks.ts @@ -3,17 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - ApplicationStart, - HttpSetup, - NotificationsStart, - PublicAppInfo, -} from 'opensearch-dashboards/public'; +import { ApplicationStart, PublicAppInfo } from 'opensearch-dashboards/public'; import { useObservable } from 'react-use'; -import { useCallback, useMemo } from 'react'; -import { i18n } from '@osd/i18n'; -import { getDirectQueryConnections, mergeDataSourcesWithConnections } from './utils'; -import { DataSource } from '../common/types'; +import { useMemo } from 'react'; export function useApplications(applicationInstance: ApplicationStart) { const applications = useObservable(applicationInstance.applications$); From 28a62aa09a65d84d37934fc99ecea772056b15ef Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 28 Aug 2024 16:46:11 +0800 Subject: [PATCH 12/24] Fix checked status disappear after modal tab change Signed-off-by: Lin Wang --- .../association_data_source_modal.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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 74e9d47628e8..5d275862b637 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 @@ -120,6 +120,7 @@ export const AssociationDataSourceModal = ({ const handleSelectionChange = useCallback( (newOptions: DataSourceModalOption[]) => { + const displayedConnectionIds = newOptions.map(({ connection }) => connection.id); const newCheckedConnectionIds = newOptions .filter(({ checked }) => checked === 'on') .map(({ connection }) => connection.id); @@ -129,12 +130,18 @@ export const AssociationDataSourceModal = ({ option = { ...option }; const checkedInNewOptions = newCheckedConnectionIds.includes(option.connection.id); const connection = option.connection; - option.checked = checkedInNewOptions ? 'on' : undefined; + // Some connections may hidden by different tab, we should not update checked status for these connections + if (displayedConnectionIds.includes(connection.id)) { + option.checked = checkedInNewOptions ? 'on' : undefined; + } + // Set option to 'on' if checked status of any child DQC become 'on' this time if (connection.connectionType === DataSourceConnectionType.OpenSearchConnection) { const childDQCIds = allConnections .filter(({ parentId }) => parentId === connection.id) - .map(({ id }) => id); + .map(({ id }) => id) + // Ensure all child DQCs has been displayed, or the checked status should not been updated + .filter((id) => displayedConnectionIds.includes(id)); // Check if there any DQC change to checked status this time, set to "on" if exists. if ( newCheckedConnectionIds.some( @@ -148,9 +155,11 @@ export const AssociationDataSourceModal = ({ } } + // Set option to 'on' if checked status of parent data source become 'on' this time if (connection.connectionType === DataSourceConnectionType.DirectQueryConnection) { const parentConnection = allConnections.find(({ id }) => id === connection.parentId); - if (parentConnection) { + // Ensure parent connection has been displayed, or the checked status should not been changed. + if (parentConnection && displayedConnectionIds.includes(parentConnection.id)) { const isParentCheckedLastTime = !!prevOptions.find( (item) => item.connection.id === parentConnection.id && item.checked === 'on' ); From ef78bf2ae6971f1cdfd42d0a8232630aabb810e0 Mon Sep 17 00:00:00 2001 From: yubonluo Date: Wed, 28 Aug 2024 22:58:12 +0800 Subject: [PATCH 13/24] optimize the dqc table Signed-off-by: yubonluo --- .../public/components/workspace_detail/a.scss | 16 --- .../workspace_detail/data_source_table.scss | 15 +++ .../opensearch_connections_table.tsx | 125 +++++------------- 3 files changed, 48 insertions(+), 108 deletions(-) delete mode 100644 src/plugins/workspace/public/components/workspace_detail/a.scss create mode 100644 src/plugins/workspace/public/components/workspace_detail/data_source_table.scss diff --git a/src/plugins/workspace/public/components/workspace_detail/a.scss b/src/plugins/workspace/public/components/workspace_detail/a.scss deleted file mode 100644 index 8286a8ad6d8e..000000000000 --- a/src/plugins/workspace/public/components/workspace_detail/a.scss +++ /dev/null @@ -1,16 +0,0 @@ -.hide-header .euiTableHeader { - display: none; - } - -.no-padding-table { - padding: 0 !important; /* 消除所有内边距 */ - margin: 0 !important; /* 消除所有外边距,视需求而定 */ -} - -.no-padding-table .euiTableCellContent { - padding: 0 !important; /* 消除单元格内边距 */ -} - -.noTableHeader thead { - display: none; - } diff --git a/src/plugins/workspace/public/components/workspace_detail/data_source_table.scss b/src/plugins/workspace/public/components/workspace_detail/data_source_table.scss new file mode 100644 index 000000000000..c3c404a4067a --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_detail/data_source_table.scss @@ -0,0 +1,15 @@ +.customized-table thead { + display: none; +} + +[id^="row"][id$="expansion"] > td:first-child > div:first-child { + padding: 0; +} + +.customized-row:first-child > td { + border-top: 0; +} + +.customized-row:last-child > td { + border-bottom: 0; +} 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 6e458f994301..c11f55cf06eb 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 @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import './data_source_table.scss'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiSpacer, @@ -21,10 +22,7 @@ import { EuiPopoverTitle, EuiSmallButton, EuiLink, - EuiSmallButtonIcon, - EuiFlexItem, - EuiFlexGroup, - EuiBasicTable, + EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; @@ -128,35 +126,14 @@ export const OpenSearchConnectionTable = ({ if (itemIdToExpandedRowMapValues[item.id]) { delete itemIdToExpandedRowMapValues[item.id]; } else { - const { relatedConnections } = item; - // itemIdToExpandedRowMapValues[item.id] = ( - // - // {relatedConnections?.map((relatedConnection) => ( - // - // {relatedConnection.name} - // {relatedConnection.type} - // {relatedConnection.description} - // - - // - // } - // /> - // ))} - // - // ); itemIdToExpandedRowMapValues[item.id] = ( - ); @@ -164,67 +141,25 @@ export const OpenSearchConnectionTable = ({ setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); }; - const columns1: Array> = [ - { - field: 'name', - name: i18n.translate('workspace.detail.dataSources.table.title', { - defaultMessage: 'Title', - }), - truncateText: true, - render: (name: string, record) => { - const origin = window.location.origin; - let url: string; - if (record.connectionType === DataSourceConnectionType.OpenSearchConnection) { - url = `${origin}/app/dataSources_core/${record.id}`; - } else { - url = `${origin}/app/dataSources_core/manage/${name}?dataSourceMDSId=${record.parentId}`; - } - return ( - - {name} - - ); - }, - }, - { - field: 'type', - name: i18n.translate('workspace.detail.dataSources.table.type', { - defaultMessage: 'Type', - }), - truncateText: true, - }, - { - field: 'description', - name: i18n.translate('workspace.detail.dataSources.table.description', { - defaultMessage: 'Description', - }), - truncateText: true, - }, - { - field: 'relatedConnections', - name: i18n.translate('workspace.detail.dataSources.table.relatedConnections', { - defaultMessage: 'Related connections', - }), - align: 'right', - truncateText: true, - render: (relatedConnections: DataSourceConnection[], record) => , - }, - ]; - - const columns: Array> = [ - { - width: '40px', - isExpander: true, - render: (item: DataSourceConnection) => - connectionType === 'directQueryConnections' && item?.relatedConnections?.length ? ( - toggleDetails(item)} - aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} - iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} - /> - ) : null, - }, + const baseColumns: Array> = [ + ...(connectionType === 'directQueryConnections' + ? [ + { + width: '40px', + isExpander: true, + render: (item: DataSourceConnection) => + item?.relatedConnections?.length ? ( + toggleDetails(item)} + aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'} + iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} + /> + ) : null, + }, + ] + : []), { + width: '25%', field: 'name', name: i18n.translate('workspace.detail.dataSources.table.title', { defaultMessage: 'Title', @@ -246,6 +181,7 @@ export const OpenSearchConnectionTable = ({ }, }, { + width: '10%', field: 'type', name: i18n.translate('workspace.detail.dataSources.table.type', { defaultMessage: 'Type', @@ -253,6 +189,7 @@ export const OpenSearchConnectionTable = ({ truncateText: true, }, { + width: '35%', field: 'description', name: i18n.translate('workspace.detail.dataSources.table.description', { defaultMessage: 'Description', @@ -311,6 +248,10 @@ export const OpenSearchConnectionTable = ({ ), }, + ]; + + const columns: Array> = [ + ...baseColumns, ...(isDashboardAdmin ? [ { @@ -338,7 +279,7 @@ export const OpenSearchConnectionTable = ({ 'data-test-subj': 'workspace-detail-dataSources-table-actions-remove', }, ], - } as EuiTableActionsColumnType, + } as EuiTableActionsColumnType, ] : []), ]; From 99de2f47946b72c6b50d7e172799b9c590e34889 Mon Sep 17 00:00:00 2001 From: yubonluo Date: Thu, 29 Aug 2024 00:07:55 +0800 Subject: [PATCH 14/24] optimize the code Signed-off-by: yubonluo --- .../data_source_management/public/index.ts | 2 +- src/plugins/workspace/common/constants.ts | 6 ++++ .../association_data_source_modal.tsx | 6 +--- .../opensearch_connections_table.tsx | 12 +++---- .../select_data_source_panel.tsx | 33 ++++++++++--------- src/plugins/workspace/public/utils.ts | 3 +- 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/plugins/data_source_management/public/index.ts b/src/plugins/data_source_management/public/index.ts index 9bb1a9cc8b42..11c2864ec86f 100644 --- a/src/plugins/data_source_management/public/index.ts +++ b/src/plugins/data_source_management/public/index.ts @@ -26,4 +26,4 @@ export { } from './components/data_source_menu'; export { DataSourceSelectionService } from './service/data_source_selection_service'; export { getDefaultDataSourceId, getDefaultDataSourceId$ } from './components/utils'; -export { DATACONNECTIONS_BASE } from './constants'; +export { DATACONNECTIONS_BASE, DatasourceTypeToDisplayName } from './constants'; diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 735c253fd0c0..2762c26ab895 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { EuiButtonGroupOptionProps } from '@elastic/eui'; import { i18n } from '@osd/i18n'; export const WORKSPACE_FATAL_ERROR_APP_ID = 'workspace_fatal_error'; @@ -147,3 +148,8 @@ export const CURRENT_USER_PLACEHOLDER = '%me%'; export const MAX_WORKSPACE_NAME_LENGTH = 40; export const MAX_WORKSPACE_DESCRIPTION_LENGTH = 200; + +export enum AssociationDataSourceModalTab { + OpenSearchConnections = 'opensearch-connections', + DirectQueryConnections = 'direction-query-connections', +} 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 f5275f19fba5..50859803f8e0 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 @@ -26,6 +26,7 @@ import { i18n } from '@osd/i18n'; import { getDataSourcesList, fetchDataSourceConnections } from '../../utils'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import { HttpStart, NotificationsStart, SavedObjectsStart } from '../../../../../core/public'; +import { AssociationDataSourceModalTab } from '../../../common/constants'; type DataSourceModalOption = EuiSelectableOption<{ connection: DataSourceConnection }>; @@ -55,11 +56,6 @@ const convertConnectionsToOptions = ( })); }; -enum AssociationDataSourceModalTab { - OpenSearchConnections = 'opensearch-connections', - DirectQueryConnections = 'direction-query-connections', -} - const tabOptions: EuiButtonGroupOptionProps[] = [ { id: AssociationDataSourceModalTab.OpenSearchConnections, 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 c11f55cf06eb..6083f557fc8e 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 @@ -28,6 +28,7 @@ import { i18n } from '@osd/i18n'; import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types'; import PrometheusLogo from '../../assets/prometheus_logo.svg'; import S3Logo from '../../assets/s3_logo.svg'; +import { AssociationDataSourceModalTab } from '../../../common/constants'; interface OpenSearchConnectionTableProps { isDashboardAdmin: boolean; @@ -45,7 +46,6 @@ export const OpenSearchConnectionTable = ({ const [selectedItems, setSelectedItems] = useState([]); const [modalVisible, setModalVisible] = useState(false); const [popoversState, setPopoversState] = useState>({}); - // const [selectedItems, setSelectedItems] = useState([]); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< Record >({}); @@ -106,9 +106,9 @@ export const OpenSearchConnectionTable = ({ const directQueryConnectionIcon = (connector: string | undefined) => { switch (connector) { - case 'S3GLUE': + case 'Amazon S3': return ; - case 'PROMETHEUS': + case 'Prometheus': return ; default: return <>; @@ -142,7 +142,7 @@ export const OpenSearchConnectionTable = ({ }; const baseColumns: Array> = [ - ...(connectionType === 'directQueryConnections' + ...(connectionType === AssociationDataSourceModalTab.DirectQueryConnections ? [ { width: '40px', @@ -169,9 +169,9 @@ export const OpenSearchConnectionTable = ({ const origin = window.location.origin; let url: string; if (record.connectionType === DataSourceConnectionType.OpenSearchConnection) { - url = `${origin}/app/dataSources_core/${record.id}`; + url = `${origin}/app/dataSources/${record.id}`; } else { - url = `${origin}/app/dataSources_core/manage/${name}?dataSourceMDSId=${record.parentId}`; + url = `${origin}/app/dataSources/manage/${name}?dataSourceMDSId=${record.parentId}`; } return ( 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 c3e35b4f8091..b6cdc07840d8 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 @@ -17,6 +17,7 @@ import { EuiLoadingSpinner, EuiButtonGroup, EuiIcon, + EuiButtonGroupOptionProps, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from 'react-intl'; @@ -28,7 +29,22 @@ import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react import { CoreStart, SavedObjectsStart, WorkspaceObject } from '../../../../../core/public'; import { convertPermissionSettingsToPermissions, useWorkspaceFormContext } from '../workspace_form'; import { fetchDataSourceConnections } from '../../utils'; +import { AssociationDataSourceModalTab } from '../../../common/constants'; +const toggleButtons: EuiButtonGroupOptionProps[] = [ + { + id: AssociationDataSourceModalTab.OpenSearchConnections, + label: i18n.translate('workspace.detail.dataSources.openSearchConnections', { + defaultMessage: 'OpenSearch connections', + }), + }, + { + id: AssociationDataSourceModalTab.DirectQueryConnections, + label: i18n.translate('workspace.detail.dataSources.directQueryConnections', { + defaultMessage: 'Direct query connections', + }), + }, +]; export interface SelectDataSourcePanelProps { savedObjects: SavedObjectsStart; assignedDataSources: DataSource[]; @@ -53,7 +69,7 @@ export const SelectDataSourceDetailPanel = ({ const [assignedDataSourceConnections, setAssignedDataSourceConnections] = useState< DataSourceConnection[] >([]); - const [toggleIdSelected, setToggleIdSelected] = useState('openSearchConnections'); + const [toggleIdSelected, setToggleIdSelected] = useState(toggleButtons[0].id); useEffect(() => { setIsLoading(true); @@ -63,21 +79,6 @@ export const SelectDataSourceDetailPanel = ({ }); }, [assignedDataSources, http, notifications]); - const toggleButtons = [ - { - id: 'openSearchConnections', - label: i18n.translate('workspace.detail.dataSources.openSearchConnections', { - defaultMessage: 'OpenSearch connections', - }), - }, - { - id: 'directQueryConnections', - label: i18n.translate('workspace.detail.dataSources.directQueryConnections', { - defaultMessage: 'Direct query connections', - }), - }, - ]; - const handleAssignDataSourceConnections = async ( dataSourceConnections: DataSourceConnection[] ) => { diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 9c5c4efd02a1..2440def2a34e 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -32,6 +32,7 @@ import { SigV4ServiceName } from '../../../plugins/data_source/common/data_sourc import { DirectQueryDatasourceDetails, DATACONNECTIONS_BASE, + DatasourceTypeToDisplayName, } from '../../data_source_management/public'; import { DataSource, DataSourceConnection, DataSourceConnectionType } from '../common/types'; import { @@ -255,7 +256,7 @@ export const getDirectQueryConnections = async (dataSourceId: string, http: Http (dataConnection: DirectQueryDatasourceDetails) => ({ id: `${dataSourceId}-${dataConnection.name}`, name: dataConnection.name, - type: dataConnection.connector, + type: DatasourceTypeToDisplayName[dataConnection.connector], connectionType: DataSourceConnectionType.DirectQueryConnection, description: dataConnection.description, parentId: dataSourceId, From 679d689262ba5ac9f792d451963aadd29bfb846f Mon Sep 17 00:00:00 2001 From: yubonluo Date: Thu, 29 Aug 2024 10:26:49 +0800 Subject: [PATCH 15/24] update the table css Signed-off-by: yubonluo --- .../public/components/workspace_detail/data_source_table.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/workspace/public/components/workspace_detail/data_source_table.scss b/src/plugins/workspace/public/components/workspace_detail/data_source_table.scss index c3c404a4067a..6b11cacd471e 100644 --- a/src/plugins/workspace/public/components/workspace_detail/data_source_table.scss +++ b/src/plugins/workspace/public/components/workspace_detail/data_source_table.scss @@ -13,3 +13,7 @@ .customized-row:last-child > td { border-bottom: 0; } + +.customized-row > td:nth-child(2) { + padding-left: 25px; +} From 97a5c9f5f0e4c88d2ca5b469bca046f1ce76d045 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 29 Aug 2024 16:24:47 +0800 Subject: [PATCH 16/24] Simplify options update logic Signed-off-by: Lin Wang --- .../association_data_source_modal.test.tsx | 303 ++++++++++++++++++ .../association_data_source_modal.tsx | 141 ++++---- 2 files changed, 383 insertions(+), 61 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.test.tsx diff --git a/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.test.tsx b/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.test.tsx new file mode 100644 index 000000000000..1ad4d552f274 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.test.tsx @@ -0,0 +1,303 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { DataSourceConnectionType } from '../../../common/types'; + +import { DataSourceModalOption, getUpdatedOptions } from './association_data_source_modal'; + +const mockPrevAllOptions = [ + { + connection: { + id: 'ds1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: undefined, + }, + { + connection: { + id: 'dqc1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', + }, + checked: undefined, + }, + { + connection: { + id: 'ds2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: 'on', + }, + { + connection: { + id: 'dqc2', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds2', + }, + checked: 'on', + }, +] as DataSourceModalOption[]; + +describe('AssociationDataSourceModal utils: getUpdatedOptions', () => { + it('should not update checked status when an option remains unchanged', () => { + const newOptions = [ + { + connection: { + id: 'ds1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + parentId: null, + }, + checked: undefined, + }, + { + connection: { + id: 'dqc1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', + }, + checked: undefined, + }, + { + connection: { + id: 'ds2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + parentId: null, + }, + checked: 'on', + }, + { + connection: { + id: 'dqc2', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds2', + }, + checked: 'on', + }, + ] as DataSourceModalOption[]; + + const updatedOptions = getUpdatedOptions({ prevAllOptions: mockPrevAllOptions, newOptions }); + + expect(updatedOptions).toEqual(mockPrevAllOptions); + }); + + it('should update checked status when a data source option is checked', () => { + const newOptions = [ + { + connection: { + id: 'ds1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: 'on', + }, + { + connection: { + id: 'ds2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: 'on', + }, + ] as DataSourceModalOption[]; + + const updatedOptions = getUpdatedOptions({ prevAllOptions: mockPrevAllOptions, newOptions }); + + expect(updatedOptions).toEqual([ + { + connection: { + id: 'ds1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: 'on', + }, + { + connection: { + id: 'dqc1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', + }, + checked: 'on', + }, + { + connection: { + id: 'ds2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: 'on', + }, + { + connection: { + id: 'dqc2', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds2', + }, + checked: 'on', + }, + ]); + }); + + it('should update checked status when a direct query connection option is checked', () => { + const newOptions = [ + { + connection: { + id: 'dqc1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', + }, + checked: 'on', + }, + { + connection: { + id: 'dqc2', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds2', + }, + checked: 'on', + }, + ] as DataSourceModalOption[]; + + const updatedOptions = getUpdatedOptions({ prevAllOptions: mockPrevAllOptions, newOptions }); + + expect(updatedOptions).toEqual([ + { + connection: { + id: 'ds1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: 'on', + }, + { + connection: { + id: 'dqc1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', + }, + checked: 'on', + }, + { + connection: { + id: 'ds2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: 'on', + }, + { + connection: { + id: 'dqc2', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds2', + }, + checked: 'on', + }, + ]); + }); + + it('should update checked status when a data source option is unchecked', () => { + const newOptions = [ + { + connection: { + id: 'ds1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: undefined, + }, + { + connection: { + id: 'ds2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: undefined, + }, + ] as DataSourceModalOption[]; + + const updatedOptions = getUpdatedOptions({ prevAllOptions: mockPrevAllOptions, newOptions }); + + expect(updatedOptions).toEqual([ + { + connection: { + id: 'ds1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: undefined, + }, + { + connection: { + id: 'dqc1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', + }, + checked: undefined, + }, + { + connection: { + id: 'ds2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: undefined, + }, + { + connection: { + id: 'dqc2', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds2', + }, + checked: undefined, + }, + ]); + }); + + it('should update checked status when a direct query connection option is unchecked', () => { + const newOptions = [ + { + connection: { + id: 'dqc1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', + }, + checked: undefined, + }, + { + connection: { + id: 'dqc2', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds2', + }, + checked: undefined, + }, + ] as DataSourceModalOption[]; + + const updatedOptions = getUpdatedOptions({ prevAllOptions: mockPrevAllOptions, newOptions }); + + expect(updatedOptions).toEqual([ + { + connection: { + id: 'ds1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: undefined, + }, + { + connection: { + id: 'dqc1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds1', + }, + checked: undefined, + }, + { + connection: { + id: 'ds2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + }, + checked: 'on', + }, + { + connection: { + id: 'dqc2', + connectionType: DataSourceConnectionType.DirectQueryConnection, + parentId: 'ds2', + }, + checked: undefined, + }, + ]); + }); +}); 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..a352b4e970b4 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 @@ -28,7 +28,7 @@ import { DataSourceConnection, DataSourceConnectionType } from '../../../common/ import { HttpStart, NotificationsStart, SavedObjectsStart } from '../../../../../core/public'; import { AssociationDataSourceModalTab } from '../../../common/constants'; -type DataSourceModalOption = EuiSelectableOption<{ connection: DataSourceConnection }>; +export type DataSourceModalOption = EuiSelectableOption<{ connection: DataSourceConnection }>; const convertConnectionsToOptions = ( connections: DataSourceConnection[], @@ -56,6 +56,82 @@ const convertConnectionsToOptions = ( })); }; +export const getUpdatedOptions = ({ + prevAllOptions, + newOptions, +}: { + prevAllOptions: DataSourceModalOption[]; + newOptions: DataSourceModalOption[]; +}) => { + let updatedOptions = prevAllOptions; + const newCheckedOptions: DataSourceModalOption[] = []; + const newUncheckedOptions: DataSourceModalOption[] = []; + + for (const option of newOptions) { + const previousOption = prevAllOptions.find( + ({ connection }) => connection.id === option.connection.id + ); + + if (previousOption?.checked === option.checked) { + continue; + } + if (option.checked === 'on') { + newCheckedOptions.push(option); + } else { + newUncheckedOptions.push(option); + } + } + + // Update checked status if option checked this time + for (const newCheckedOption of newCheckedOptions) { + switch (newCheckedOption.connection.connectionType) { + case DataSourceConnectionType.OpenSearchConnection: + // Set data source and its DQC checked status to 'on' + updatedOptions = updatedOptions.map((option) => + option.connection.parentId === newCheckedOption.connection.id || + option.connection.id === newCheckedOption.connection.id + ? { ...option, checked: 'on' } + : option + ); + break; + case DataSourceConnectionType.DirectQueryConnection: + // Set DQC and its parent data source checked status to 'on' + updatedOptions = updatedOptions.map((option) => + option.connection.id === newCheckedOption.connection.id || + option.connection.id === newCheckedOption.connection.parentId + ? { ...option, checked: 'on' } + : option + ); + break; + } + } + + // Update checked status if option unchecked this time + for (const newUncheckedOption of newUncheckedOptions) { + switch (newUncheckedOption.connection.connectionType) { + case DataSourceConnectionType.OpenSearchConnection: + // Set data source and its DQC checked status to undefined + updatedOptions = updatedOptions.map((option) => + option.connection.parentId === newUncheckedOption.connection.id || + option.connection.id === newUncheckedOption.connection.id + ? { ...option, checked: undefined } + : option + ); + break; + case DataSourceConnectionType.DirectQueryConnection: + // Set DQC checked status to 'undefined' + updatedOptions = updatedOptions.map((option) => + option.connection.id === newUncheckedOption.connection.id + ? { ...option, checked: undefined } + : option + ); + break; + } + } + + return updatedOptions; +}; + const tabOptions: EuiButtonGroupOptionProps[] = [ { id: AssociationDataSourceModalTab.OpenSearchConnections, @@ -114,66 +190,9 @@ export const AssociationDataSourceModal = ({ [allOptions] ); - const handleSelectionChange = useCallback( - (newOptions: DataSourceModalOption[]) => { - const displayedConnectionIds = newOptions.map(({ connection }) => connection.id); - const newCheckedConnectionIds = newOptions - .filter(({ checked }) => checked === 'on') - .map(({ connection }) => connection.id); - - setAllOptions((prevOptions) => { - return prevOptions.map((option) => { - option = { ...option }; - const checkedInNewOptions = newCheckedConnectionIds.includes(option.connection.id); - const connection = option.connection; - // Some connections may hidden by different tab, we should not update checked status for these connections - if (displayedConnectionIds.includes(connection.id)) { - option.checked = checkedInNewOptions ? 'on' : undefined; - } - - // Set option to 'on' if checked status of any child DQC become 'on' this time - if (connection.connectionType === DataSourceConnectionType.OpenSearchConnection) { - const childDQCIds = allConnections - .filter(({ parentId }) => parentId === connection.id) - .map(({ id }) => id) - // Ensure all child DQCs has been displayed, or the checked status should not been updated - .filter((id) => displayedConnectionIds.includes(id)); - // Check if there any DQC change to checked status this time, set to "on" if exists. - if ( - newCheckedConnectionIds.some( - (id) => - childDQCIds.includes(id) && - // This child DQC not checked before - !prevOptions.find((item) => item.connection.id === id && item.checked === 'on') - ) - ) { - option.checked = 'on'; - } - } - - // Set option to 'on' if checked status of parent data source become 'on' this time - if (connection.connectionType === DataSourceConnectionType.DirectQueryConnection) { - const parentConnection = allConnections.find(({ id }) => id === connection.parentId); - // Ensure parent connection has been displayed, or the checked status should not been changed. - if (parentConnection && displayedConnectionIds.includes(parentConnection.id)) { - const isParentCheckedLastTime = !!prevOptions.find( - (item) => item.connection.id === parentConnection.id && item.checked === 'on' - ); - const isParentCheckedThisTime = newCheckedConnectionIds.includes(parentConnection.id); - - // Update checked status if parent checked status changed this time - if (isParentCheckedLastTime !== isParentCheckedThisTime) { - option.checked = isParentCheckedThisTime ? 'on' : undefined; - } - } - } - - return option; - }); - }); - }, - [allConnections] - ); + const handleSelectionChange = useCallback((newOptions: DataSourceModalOption[]) => { + setAllOptions((prevAllOptions) => getUpdatedOptions({ prevAllOptions, newOptions })); + }, []); useEffect(() => { setIsLoading(true); From c32b96078037c35a02cec47c55fdceb20ca99862 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 29 Aug 2024 17:51:14 +0800 Subject: [PATCH 17/24] Add unit tests for AssociationDataSourceModal Signed-off-by: Lin Wang --- .../association_data_source_modal.test.tsx | 159 +++++++++++++++++- .../association_data_source_modal.tsx | 14 +- 2 files changed, 167 insertions(+), 6 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.test.tsx b/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.test.tsx index 1ad4d552f274..76c8d6692f5c 100644 --- a/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.test.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/association_data_source_modal.test.tsx @@ -2,9 +2,20 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import { IntlProvider } from 'react-intl'; + import { DataSourceConnectionType } from '../../../common/types'; +import { coreMock } from '../../../../../core/public/mocks'; +import * as utilsExports from '../../utils'; -import { DataSourceModalOption, getUpdatedOptions } from './association_data_source_modal'; +import { + getUpdatedOptions, + DataSourceModalOption, + AssociationDataSourceModal, + AssociationDataSourceModalProps, +} from './association_data_source_modal'; const mockPrevAllOptions = [ { @@ -301,3 +312,149 @@ describe('AssociationDataSourceModal utils: getUpdatedOptions', () => { ]); }); }); + +const setupAssociationDataSourceModal = ({ + assignedConnections, + handleAssignDataSourceConnections, +}: Partial = {}) => { + const coreServices = coreMock.createStart(); + jest.spyOn(utilsExports, 'getDataSourcesList').mockResolvedValue([]); + jest.spyOn(utilsExports, 'fetchDataSourceConnections').mockResolvedValue([ + { + id: 'ds1', + name: 'Data Source 1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', + relatedConnections: [ + { + id: 'ds1-dqc1', + name: 'dqc1', + parentId: 'ds1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + type: 'S3', + }, + ], + }, + { + id: 'ds1-dqc1', + name: 'dqc1', + parentId: 'ds1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + type: 'S3', + }, + { + id: 'ds2', + name: 'Data Source 2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', + }, + ]); + render( + + + + ); + return {}; +}; + +describe('AssociationDataSourceModal', () => { + 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 display data sources options by default', async () => { + setupAssociationDataSourceModal(); + expect(screen.getByText('Associate OpenSearch connections')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByRole('option', { name: 'Data Source 1' })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: 'Data Source 2' })).toBeInTheDocument(); + }); + }); + + it('should hide associated data sources', async () => { + setupAssociationDataSourceModal({ + assignedConnections: [ + { + id: 'ds2', + name: 'Data Source 2', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', + }, + ], + }); + expect(screen.getByText('Associate OpenSearch connections')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByRole('option', { name: 'Data Source 1' })).toBeInTheDocument(); + expect(screen.queryByRole('option', { name: 'Data Source 2' })).not.toBeInTheDocument(); + }); + }); + + it('should call handleAssignDataSourceConnections with data source and DQCs after data source assigned', async () => { + const handleAssignDataSourceConnectionsMock = jest.fn(); + setupAssociationDataSourceModal({ + handleAssignDataSourceConnections: handleAssignDataSourceConnectionsMock, + }); + + await waitFor(() => { + fireEvent.click(screen.getByRole('option', { name: 'Data Source 1' })); + fireEvent.click(screen.getByRole('button', { name: 'Associate data sources' })); + }); + + expect(handleAssignDataSourceConnectionsMock).toHaveBeenCalledWith([ + { + id: 'ds1', + name: 'Data Source 1', + connectionType: DataSourceConnectionType.OpenSearchConnection, + type: 'OpenSearch', + relatedConnections: [ + { + id: 'ds1-dqc1', + name: 'dqc1', + parentId: 'ds1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + type: 'S3', + }, + ], + }, + { + id: 'ds1-dqc1', + name: 'dqc1', + parentId: 'ds1', + connectionType: DataSourceConnectionType.DirectQueryConnection, + type: 'S3', + }, + ]); + }); +}); 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 a352b4e970b4..a19bc605cd1a 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 @@ -135,13 +135,13 @@ export const getUpdatedOptions = ({ const tabOptions: EuiButtonGroupOptionProps[] = [ { id: AssociationDataSourceModalTab.OpenSearchConnections, - label: i18n.translate('workspace.form.selectDataSource.subTitle', { + label: i18n.translate('workspace.form.dataSourceModal.tab.openSearchConnections', { defaultMessage: 'OpenSearch connections', }), }, { id: AssociationDataSourceModalTab.DirectQueryConnections, - label: i18n.translate('workspace.form.selectDataSource.subTitle', { + label: i18n.translate('workspace.form.dataSourceModal.tab.directQueryConnections', { defaultMessage: 'Direct query connections', }), }, @@ -153,7 +153,7 @@ export interface AssociationDataSourceModalProps { savedObjects: SavedObjectsStart; assignedConnections: DataSourceConnection[]; closeModal: () => void; - handleAssignDataSourceConnections: (connections: DataSourceConnection[]) => Promise; + handleAssignDataSourceConnections: (connections: DataSourceConnection[]) => void; } export const AssociationDataSourceModal = ({ @@ -229,10 +229,14 @@ export const AssociationDataSourceModal = ({ setCurrentTab(id)} + onChange={(id) => { + setCurrentTab(id); + }} buttonSize="compressed" /> From f6d240ded663395f57447acb45304afb0c120dc2 Mon Sep 17 00:00:00 2001 From: yubonluo Date: Fri, 30 Aug 2024 14:45:47 +0800 Subject: [PATCH 18/24] add unit test Signed-off-by: yubonluo --- .../workspace_detail.test.tsx.snap | 2 +- .../association_data_source_modal.test.tsx | 12 +- .../association_data_source_modal.tsx | 5 +- ...scss => data_source_connection_table.scss} | 0 ...e.tsx => data_source_connection_table.tsx} | 16 +- .../select_data_source_panel.test.tsx | 313 ++++++++++++------ .../select_data_source_panel.tsx | 4 +- .../workspace_detail.test.tsx | 4 +- 8 files changed, 235 insertions(+), 121 deletions(-) rename src/plugins/workspace/public/components/workspace_detail/{data_source_table.scss => data_source_connection_table.scss} (100%) rename src/plugins/workspace/public/components/workspace_detail/{opensearch_connections_table.tsx => data_source_connection_table.tsx} (95%) diff --git a/src/plugins/workspace/public/components/workspace_detail/__snapshots__/workspace_detail.test.tsx.snap b/src/plugins/workspace/public/components/workspace_detail/__snapshots__/workspace_detail.test.tsx.snap index cf670fddf4df..69cb1351fd18 100644 --- a/src/plugins/workspace/public/components/workspace_detail/__snapshots__/workspace_detail.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_detail/__snapshots__/workspace_detail.test.tsx.snap @@ -180,7 +180,7 @@ exports[`WorkspaceDetail render workspace detail page normally 1`] = ` - Data Sources + Data sources