Skip to content

Commit

Permalink
Merge pull request opensearch-project#1 from Kapian1234/associate_dat…
Browse files Browse the repository at this point in the history
…asource_modal

Refactor association modal
  • Loading branch information
yubonluo authored Aug 27, 2024
2 parents 0ef40b6 + 2409005 commit 20db731
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 ? (
<EuiBadge>
{i18n.translate('workspace.form.selectDataSource.optionBadge', {
defaultMessage: '+ {relatedConnections} related',
values: {
relatedConnections: connection.relatedConnections.length,
},
})}
</EuiBadge>
) : 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<void>;
handleAssignDataSourceConnections: (connections: DataSourceConnection[]) => Promise<void>;
}

export const AssociationDataSourceModal = ({
http,
notifications,
closeModal,
savedObjects,
assignedDataSources,
handleAssignDataSources,
assignedConnections,
handleAssignDataSourceConnections,
}: AssociationDataSourceModalProps) => {
const [options, setOptions] = useState<EuiSelectableOption[]>([]);
const [allDataSources, setAllDataSources] = useState<DataSource[]>([]);
const [allConnections, setAllConnections] = useState<DataSourceConnection[]>([]);
const [currentTab, setCurrentTab] = useState('all');
const [allOptions, setAllOptions] = useState<DataSourceModalOption[]>([]);
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 (
<EuiModal onClose={closeModal}>
<EuiModal onClose={closeModal} style={{ width: 900 }}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<h1>
<FormattedMessage
id="workspace.detail.dataSources.associateModal.title"
defaultMessage="Associate OpenSearch connections"
/>
</h1>
<FormattedMessage
id="workspace.detail.dataSources.associateModal.title"
defaultMessage="Associate OpenSearch connections"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText size="s" color="subdued">
<EuiText size="xs" color="subdued">
<FormattedMessage
id="workspace.detail.dataSources.associateModal.message"
defaultMessage="Add OpenSearch connections that will be available in the workspace."
/>
</EuiText>
<EuiSpacer />
<EuiButtonGroup
legend="Data source tab"
options={tabOptions}
idSelected={currentTab}
onChange={(id) => setCurrentTab(id)}
buttonSize="compressed"
/>
<EuiSpacer size="s" />
<EuiSelectable
aria-label="Searchable"
searchable
listProps={{ bordered: true }}
listProps={{ bordered: true, onFocusBadge: false }}
searchProps={{
'data-test-subj': 'workspace-detail-dataSources-associateModal-search',
}}
options={options}
onChange={(newOptions) => setOptions(newOptions)}
onChange={handleSelectionChange}
isLoading={isLoading}
>
{(list, search) => (
<Fragment>
Expand All @@ -99,20 +245,20 @@ export const AssociationDataSourceModal = ({
</EuiModalBody>

<EuiModalFooter>
<EuiButton onClick={closeModal} fill>
<EuiButton onClick={closeModal}>
<FormattedMessage
id="workspace.detail.dataSources.associateModal.close.button"
defaultMessage="Close"
/>
</EuiButton>
<EuiButton
onClick={() => handleAssignDataSources(selectedDataSources)}
isDisabled={!selectedDataSources || selectedDataSources.length === 0}
onClick={() => handleAssignDataSourceConnections(selectedConnections)}
isDisabled={!selectedConnections || selectedConnections.length === 0}
fill
>
<FormattedMessage
id="workspace.detail.dataSources.associateModal.save.button"
defaultMessage="Save changes"
defaultMessage="Associate data sources"
/>
</EuiButton>
</EuiModalFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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<DataSourceConnection[]>([]);
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 = [
{
Expand All @@ -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);
Expand Down Expand Up @@ -228,7 +241,7 @@ export const SelectDataSourceDetailPanel = ({
<OpenSearchConnectionTable
isDashboardAdmin={isDashboardAdmin}
connectionType={toggleIdSelected}
dataSourceConnections={dataSourceConnections}
dataSourceConnections={assignedDataSourceConnections}
handleUnassignDataSources={handleUnassignDataSources}
/>
);
Expand Down Expand Up @@ -262,10 +275,12 @@ export const SelectDataSourceDetailPanel = ({
{renderTableContent()}
{isVisible && (
<AssociationDataSourceModal
http={http}
notifications={notifications}
savedObjects={savedObjects}
assignedDataSources={assignedDataSources}
closeModal={() => setIsVisible(false)}
handleAssignDataSources={handleAssignDataSources}
assignedConnections={assignedDataSourceConnections}
handleAssignDataSourceConnections={handleAssignDataSourceConnections}
/>
)}
</EuiPanel>
Expand Down
Loading

0 comments on commit 20db731

Please sign in to comment.