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..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
@@ -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,79 +15,225 @@ import {
EuiModalHeader,
EuiModalHeaderTitle,
EuiSelectableOption,
+ EuiSpacer,
+ EuiButtonGroup,
+ EuiButtonGroupOptionProps,
+ EuiBadge,
} from '@elastic/eui';
import { FormattedMessage } from 'react-intl';
-import { getDataSourcesList } from '../../utils';
-import { DataSource } from '../../../common/types';
-import { SavedObjectsStart } from '../../../../../core/public';
+import { i18n } from '@osd/i18n';
+
+import { getDataSourcesList, fetchDataSourceConnections } from '../../utils';
+import { DataSourceConnection, DataSourceConnectionType } from '../../../common/types';
+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: AssociationDataSourceModalTab.All,
+ label: i18n.translate('workspace.form.selectDataSource.subTitle', {
+ defaultMessage: 'All',
+ }),
+ },
+ {
+ id: AssociationDataSourceModalTab.OpenSearchConnections,
+ label: i18n.translate('workspace.form.selectDataSource.subTitle', {
+ defaultMessage: 'OpenSearch connections',
+ }),
+ },
+ {
+ id: AssociationDataSourceModalTab.DirectQueryConnections,
+ label: i18n.translate('workspace.form.selectDataSource.subTitle', {
+ defaultMessage: 'Direct query connections',
+ }),
+ },
+];
export interface AssociationDataSourceModalProps {
+ http: HttpStart | undefined;
+ notifications: NotificationsStart | undefined;
savedObjects: SavedObjectsStart;
- assignedDataSources: DataSource[];
+ assignedConnections: DataSourceConnection[];
closeModal: () => void;
- handleAssignDataSources: (dataSources: DataSource[]) => Promise;
+ handleAssignDataSourceConnections: (connections: DataSourceConnection[]) => Promise;
}
export const AssociationDataSourceModal = ({
+ http,
+ notifications,
closeModal,
savedObjects,
- assignedDataSources,
- handleAssignDataSources,
+ assignedConnections,
+ handleAssignDataSourceConnections,
}: AssociationDataSourceModalProps) => {
- const [options, setOptions] = useState([]);
- const [allDataSources, setAllDataSources] = useState([]);
+ const [allConnections, setAllConnections] = useState([]);
+ const [currentTab, setCurrentTab] = useState('all');
+ const [allOptions, setAllOptions] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
- useEffect(() => {
- getDataSourcesList(savedObjects.client, ['*']).then((result) => {
- const filteredDataSources = result.filter(
- ({ id }: DataSource) => !assignedDataSources.some((ds) => ds.id === id)
+ const options = useMemo(() => {
+ if (currentTab === AssociationDataSourceModalTab.OpenSearchConnections) {
+ return allOptions.filter(
+ ({ connection }) =>
+ connection.connectionType === DataSourceConnectionType.OpenSearchConnection
);
- setAllDataSources(filteredDataSources);
- setOptions(
- filteredDataSources.map((dataSource) => ({
- label: dataSource.title,
- key: dataSource.id,
- }))
+ }
+ if (currentTab === AssociationDataSourceModalTab.DirectQueryConnections) {
+ return allOptions.filter(
+ ({ connection }) =>
+ connection.connectionType === DataSourceConnectionType.DirectQueryConnection
);
- });
- }, [assignedDataSources, savedObjects]);
+ }
+ return allOptions;
+ }, [allOptions, currentTab]);
+
+ const selectedConnections = useMemo(
+ () => allOptions.filter(({ checked }) => checked === 'on').map(({ connection }) => connection),
+ [allOptions]
+ );
+
+ const handleSelectionChange = useCallback(
+ (newOptions: DataSourceModalOption[]) => {
+ const newCheckedConnectionIds = newOptions
+ .filter(({ checked }) => checked === 'on')
+ .map(({ connection }) => connection.id);
- const selectedDataSources = useMemo(() => {
- const selectedIds = options
- .filter((option: EuiSelectableOption) => option.checked)
- .map((option: EuiSelectableOption) => option.key);
+ setAllOptions((prevOptions) => {
+ return prevOptions.map((option) => {
+ option = { ...option };
+ const checkedInNewOptions = newCheckedConnectionIds.includes(option.connection.id);
+ const connection = option.connection;
+ option.checked = checkedInNewOptions ? 'on' : undefined;
- return allDataSources.filter((ds) => selectedIds.includes(ds.id));
- }, [options, allDataSources]);
+ 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 (
+ 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';
+ }
+ }
+
+ if (connection.connectionType === DataSourceConnectionType.DirectQueryConnection) {
+ const parentConnection = allConnections.find(({ id }) => id === connection.parentId);
+ if (parentConnection) {
+ const isParentCheckedLastTime = !!prevOptions.find(
+ (item) => item.connection.id === parentConnection.id && item.checked === 'on'
+ );
+ 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;
+ }
+ }
+ }
+
+ return option;
+ });
+ });
+ },
+ [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 (
-
+
-
-
-
+
-
+
+
+ setCurrentTab(id)}
+ buttonSize="compressed"
+ />
+
setOptions(newOptions)}
+ onChange={handleSelectionChange}
+ isLoading={isLoading}
>
{(list, search) => (
@@ -99,20 +245,20 @@ export const AssociationDataSourceModal = ({
-
+
handleAssignDataSources(selectedDataSources)}
- isDisabled={!selectedDataSources || selectedDataSources.length === 0}
+ onClick={() => handleAssignDataSourceConnections(selectedConnections)}
+ isDisabled={!selectedConnections || selectedConnections.length === 0}
fill
>
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 894bd93ebc86..0eb67ba52dea 100644
--- a/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx
+++ b/src/plugins/workspace/public/components/workspace_detail/select_data_source_panel.tsx
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiText,
EuiTitle,
@@ -20,14 +20,14 @@ import {
} from '@elastic/eui';
import { i18n } from '@osd/i18n';
import { FormattedMessage } from 'react-intl';
-import { DataSource, DataSourceConnection } from '../../../common/types';
+import { DataSource, DataSourceConnection, DataSourceConnectionType } 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 { useFetchDQC } from '../../hooks';
+import { fetchDataSourceConnections } from '../../utils';
export interface SelectDataSourcePanelProps {
savedObjects: SavedObjectsStart;
@@ -50,17 +50,18 @@ export const SelectDataSourceDetailPanel = ({
const { formData, setSelectedDataSources } = useWorkspaceFormContext();
const [isLoading, setIsLoading] = useState(false);
const [isVisible, setIsVisible] = useState(false);
- const [dataSourceConnections, setDataSourceConnections] = useState([]);
+ 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 [];
+ }
+};