From 726fb0e34704dbd54c71f9e716654323d67a302e Mon Sep 17 00:00:00 2001 From: "Yuanqi(Ella) Zhu" <53279298+zhyuanqi@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:06:39 -0700 Subject: [PATCH] Add default icon for selectable component and make sure the default datasource shows automatically (#6327) Signed-off-by: Yuanqi(Ella) Zhu --- CHANGELOG.md | 1 + .../create_data_source_menu.tsx | 5 +- .../data_source_menu/data_source_menu.tsx | 3 +- .../components/data_source_menu/types.ts | 2 + .../data_source_selectable.test.tsx.snap | 22 ++- .../data_source_selectable.test.tsx | 14 +- .../data_source_selectable.tsx | 134 ++++++++++++------ .../public/components/utils.ts | 2 +- .../public/plugin.test.ts | 4 + .../data_source_management/public/plugin.ts | 2 +- 10 files changed, 134 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75a1b4923452..841516d7ebd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Add multi data source support to sample vega visualizations ([#6218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6218)) - [Multiple Datasource] Fetch data source title for DataSourceView when only id is provided ([#6315](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6315) - [Workspace] Add permission control logic ([#6052](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6052)) +- [Multiple Datasource] Add default icon for selectable component and make sure the default datasource shows automatically ([#6327](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6327)) - [Multiple Datasource] Pass selected data sources to plugin consumers when the multi-select component initially loads ([#6333](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6333)) - [Workspace] Add APIs to support plugin state in request ([#6303](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6303)) diff --git a/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.tsx b/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.tsx index 2e76c2cf23c6..484f3e6630d8 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.tsx +++ b/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.tsx @@ -5,17 +5,18 @@ import React from 'react'; import { EuiHeaderLinks } from '@elastic/eui'; +import { IUiSettingsClient } from 'src/core/public'; import { DataSourceMenu } from './data_source_menu'; import { DataSourceMenuProps } from './types'; import { MountPointPortal } from '../../../../opensearch_dashboards_react/public'; -export function createDataSourceMenu() { +export function createDataSourceMenu(uiSettings: IUiSettingsClient) { return (props: DataSourceMenuProps) => { if (props.setMenuMountPoint) { return ( - + ); diff --git a/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx b/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx index b56063e5d25f..b1f7ed1eaadd 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx +++ b/src/plugins/data_source_management/public/components/data_source_menu/data_source_menu.tsx @@ -19,7 +19,7 @@ import { import { DataSourceSelectable } from '../data_source_selectable'; export function DataSourceMenu(props: DataSourceMenuProps): ReactElement | null { - const { componentType, componentConfig } = props; + const { componentType, componentConfig, uiSettings } = props; function renderDataSourceView(config: DataSourceViewConfig): ReactElement | null { const { activeOption, fullWidth, savedObjects, notifications } = config; @@ -75,6 +75,7 @@ export function DataSourceMenu(props: DataSourceMenuProps): ReactElement | dataSourceFilter={dataSourceFilter} hideLocalCluster={hideLocalCluster || false} fullWidth={fullWidth} + uiSettings={uiSettings} /> ); } diff --git a/src/plugins/data_source_management/public/components/data_source_menu/types.ts b/src/plugins/data_source_management/public/components/data_source_menu/types.ts index 4121edc0c863..ba274f5178de 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/types.ts +++ b/src/plugins/data_source_management/public/components/data_source_menu/types.ts @@ -7,6 +7,7 @@ import { NotificationsStart, SavedObjectsClientContract, SavedObject, + IUiSettingsClient, } from '../../../../../core/public'; import { DataSourceAttributes } from '../../types'; @@ -23,6 +24,7 @@ export interface DataSourceBaseConfig { export interface DataSourceMenuProps { componentType: DataSourceComponentType; componentConfig: T; + uiSettings?: IUiSettingsClient; setMenuMountPoint?: (menuMount: MountPoint | undefined) => void; } diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/__snapshots__/data_source_selectable.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_selectable/__snapshots__/data_source_selectable.test.tsx.snap index 2cb8dff8a94d..f76958715e77 100644 --- a/src/plugins/data_source_management/public/components/data_source_selectable/__snapshots__/data_source_selectable.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_selectable/__snapshots__/data_source_selectable.test.tsx.snap @@ -35,6 +35,11 @@ exports[`DataSourceSelectable should filter options if configured 1`] = ` - Local cluster + } @@ -170,6 +183,11 @@ exports[`DataSourceSelectable should render normally with local cluster not hidd
{ let component: ShallowWrapper, React.Component<{}, {}, any>>; @@ -109,6 +110,7 @@ describe('DataSourceSelectable', () => { it('should callback if changed state', async () => { const onSelectedDataSource = jest.fn(); + spyOn(utils, 'getDefaultDataSource').and.returnValue([{ id: 'test2', label: 'test2' }]); const container = mount( { const containerInstance = container.instance(); containerInstance.onChange([{ id: 'test2', label: 'test2' }]); - expect(onSelectedDataSource).toBeCalledTimes(0); + expect(onSelectedDataSource).toBeCalledTimes(1); expect(containerInstance.state).toEqual({ dataSourceOptions: [ { @@ -133,11 +135,12 @@ describe('DataSourceSelectable', () => { label: 'test2', }, ], + defaultDataSource: null, isPopoverOpen: false, selectedOption: [ { - id: '', - label: 'Local cluster', + id: 'test2', + label: 'test2', }, ], }); @@ -151,6 +154,7 @@ describe('DataSourceSelectable', () => { label: 'test2', }, ], + defaultDataSource: null, isPopoverOpen: false, selectedOption: [ { @@ -160,7 +164,9 @@ describe('DataSourceSelectable', () => { }, ], }); + expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]); - expect(onSelectedDataSource).toBeCalledTimes(1); + expect(onSelectedDataSource).toHaveBeenCalled(); + expect(utils.getDefaultDataSource).toHaveBeenCalled(); }); }); diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx index e0fccf9aa226..5a4591597ab6 100644 --- a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.tsx @@ -12,9 +12,16 @@ import { EuiButtonEmpty, EuiSelectable, EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, } from '@elastic/eui'; -import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public'; -import { getDataSourcesWithFields } from '../utils'; +import { + IUiSettingsClient, + SavedObjectsClientContract, + ToastsStart, +} from 'opensearch-dashboards/public'; +import { getDataSourcesWithFields, getDefaultDataSource } from '../utils'; import { LocalCluster } from '../data_source_selector/data_source_selector'; import { SavedObject } from '../../../../../core/public'; import { DataSourceAttributes } from '../../types'; @@ -29,16 +36,18 @@ interface DataSourceSelectableProps { fullWidth: boolean; selectedOption?: DataSourceOption[]; dataSourceFilter?: (dataSource: SavedObject) => boolean; + uiSettings?: IUiSettingsClient; } interface DataSourceSelectableState { dataSourceOptions: SelectedDataSourceOption[]; isPopoverOpen: boolean; selectedOption?: SelectedDataSourceOption[]; + defaultDataSource: string | null; } interface SelectedDataSourceOption extends DataSourceOption { - checked?: boolean; + checked?: string; } export class DataSourceSelectable extends React.Component< @@ -53,11 +62,8 @@ export class DataSourceSelectable extends React.Component< this.state = { dataSourceOptions: [], isPopoverOpen: false, - selectedOption: this.props.selectedOption - ? this.props.selectedOption - : this.props.hideLocalCluster - ? [] - : [LocalCluster], + selectedOption: [], + defaultDataSource: null, }; this.onChange.bind(this); @@ -77,44 +83,72 @@ export class DataSourceSelectable extends React.Component< async componentDidMount() { this._isMounted = true; - getDataSourcesWithFields(this.props.savedObjectsClient, ['id', 'title', 'auth.type']) - .then((fetchedDataSources) => { - if (fetchedDataSources?.length) { - let filteredDataSources: Array> = []; - if (this.props.dataSourceFilter) { - filteredDataSources = fetchedDataSources.filter((ds) => - this.props.dataSourceFilter!(ds) - ); - } - - if (filteredDataSources.length === 0) { - filteredDataSources = fetchedDataSources; - } - - const dataSourceOptions = filteredDataSources - .map((dataSource) => ({ - id: dataSource.id, - label: dataSource.attributes?.title || '', - })) - .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())); - if (!this.props.hideLocalCluster) { - dataSourceOptions.unshift(LocalCluster); - } - - if (!this._isMounted) return; - this.setState({ - ...this.state, - dataSourceOptions, - }); - } - }) - .catch(() => { - this.props.notifications.addWarning( - i18n.translate('dataSource.fetchDataSourceError', { - defaultMessage: 'Unable to fetch existing data sources', + try { + let filteredDataSources: Array> = []; + let dataSourceOptions: DataSourceOption[] = []; + + // Fetch data sources with fields + const fetchedDataSources = await getDataSourcesWithFields(this.props.savedObjectsClient, [ + 'id', + 'title', + 'auth.type', + ]); + + if (fetchedDataSources?.length) { + filteredDataSources = this.props.dataSourceFilter + ? fetchedDataSources.filter((ds) => this.props.dataSourceFilter!(ds)) + : fetchedDataSources; + dataSourceOptions = filteredDataSources + .map((dataSource) => ({ + id: dataSource.id, + label: dataSource.attributes?.title || '', + })) + .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())); + } + + // Add local cluster to the list of data sources if it is not hidden. + if (!this.props.hideLocalCluster) { + dataSourceOptions.unshift(LocalCluster); + } + + const defaultDataSource = this.props.uiSettings?.get('defaultDataSource', null) ?? null; + const selectedDataSource = getDefaultDataSource( + filteredDataSources, + LocalCluster, + this.props.uiSettings, + this.props.hideLocalCluster, + this.props.selectedOption + ); + + if (selectedDataSource.length === 0) { + this.props.notifications.addWarning('No connected data source available.'); + } else { + // Update the checked status of the selected data source. + const updatedDataSourceOptions: SelectedDataSourceOption[] = dataSourceOptions.map( + (option) => ({ + ...option, + ...(option.id === selectedDataSource[0].id && { checked: 'on' }), }) ); - }); + + if (!this._isMounted) return; + + this.setState({ + ...this.state, + dataSourceOptions: updatedDataSourceOptions, + selectedOption: selectedDataSource, + defaultDataSource, + }); + + this.props.onSelectedDataSources(selectedDataSource); + } + } catch (error) { + this.props.notifications.addWarning( + i18n.translate('dataSource.fetchDataSourceError', { + defaultMessage: 'Unable to fetch existing data sources', + }) + ); + } } onChange(options: SelectedDataSourceOption[]) { @@ -168,7 +202,7 @@ export class DataSourceSelectable extends React.Component< data-test-subj={'dataSourceSelectableContextMenuPopover'} > - + this.onChange(newOptions)} singleSelection={true} data-test-subj={'dataSourceSelectable'} + renderOption={(option) => ( + + {option.label} + {option.id === this.state.defaultDataSource && ( + + Default + + )} + + )} > {(list, search) => ( <> diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index 50960936e222..6cfaaa9a081a 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -16,7 +16,7 @@ import { noAuthCredentialAuthMethod, } from '../types'; import { AuthenticationMethodRegistry } from '../auth_registry'; -import { DataSourceOption } from './data_source_selector/data_source_selector'; +import { DataSourceOption } from './data_source_menu/types'; export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient diff --git a/src/plugins/data_source_management/public/plugin.test.ts b/src/plugins/data_source_management/public/plugin.test.ts index ecad5f38922c..389d7027f585 100644 --- a/src/plugins/data_source_management/public/plugin.test.ts +++ b/src/plugins/data_source_management/public/plugin.test.ts @@ -21,5 +21,9 @@ describe('#dataSourceManagement', () => { const start = doStart(); const registry = start.getAuthenticationMethodRegistry(); expect(registry.getAuthenticationMethod('typeA')).toEqual(typeA); + expect(setup.ui).toEqual({ + DataSourceSelector: expect.any(Function), + getDataSourceMenu: expect.any(Function), + }); }); }); diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index c6a978ae7b61..d07911dacfc4 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -104,7 +104,7 @@ export class DataSourceManagementPlugin registerAuthenticationMethod, ui: { DataSourceSelector: createDataSourceSelector(uiSettings), - getDataSourceMenu: () => createDataSourceMenu(), + getDataSourceMenu: () => createDataSourceMenu(uiSettings), }, }; }