diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b1f2274ea3..aa3de4587fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Enhanced data source selector with default datasource shows as first choice ([#6293](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6293)) - [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) +- [Multiple Datasource] Get data source label when only id is provided in DataSourceSelectable ([#6358](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6358) - [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)) diff --git a/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap index 28d689c660d..31ae3a99d9c 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_menu/__snapshots__/create_data_source_menu.test.tsx.snap @@ -29,9 +29,7 @@ Object { /> - Local cluster - + /> @@ -63,9 +61,7 @@ Object { /> - Local cluster - + /> diff --git a/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.test.tsx b/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.test.tsx index 75a223363a1..6d7200b182c 100644 --- a/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_menu/create_data_source_menu.test.tsx @@ -74,8 +74,7 @@ describe('create data source menu', () => { perPage: 10000, type: 'data-source', }); - expect(notifications.toasts.addWarning).toBeCalledTimes(0); - expect(getByText(component.container, 'Local cluster')).toBeInTheDocument(); + expect(notifications.toasts.addWarning).toBeCalledTimes(2); }); }); 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 f76958715e7..adc2929a7bc 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 @@ -96,9 +96,7 @@ exports[`DataSourceSelectable should render normally with local cluster is hidde iconType="database" onClick={[Function]} size="s" - > - - + /> } closePopover={[Function]} @@ -162,9 +160,7 @@ exports[`DataSourceSelectable should render normally with local cluster not hidd iconType="database" onClick={[Function]} size="s" - > - - + /> } closePopover={[Function]} diff --git a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx index bbbd40c8585..efa1215ed11 100644 --- a/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selectable/data_source_selectable.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { DataSourceSelectable } from './data_source_selectable'; import { AuthType } from '../../types'; import { getDataSourcesWithFieldsResponse, mockResponseForSavedObjectsCalls } from '../../mocks'; -import { render } from '@testing-library/react'; +import { getByRole, render } from '@testing-library/react'; import * as utils from '../utils'; describe('DataSourceSelectable', () => { @@ -169,4 +169,226 @@ describe('DataSourceSelectable', () => { expect(onSelectedDataSource).toHaveBeenCalled(); expect(utils.getDefaultDataSource).toHaveBeenCalled(); }); + + it('should display selected option label normally', async () => { + const onSelectedDataSource = jest.fn(); + const container = render( + ds.attributes.auth.type !== AuthType.NoAuth} + /> + ); + + await nextTick(); + const button = await container.findByTestId('dataSourceSelectableContextMenuHeaderLink'); + expect(button).toHaveTextContent('test2'); + }); + + it('should render normally even only provide dataSourceId', async () => { + const onSelectedDataSource = jest.fn(); + const container = render( + ds.attributes.auth.type !== AuthType.NoAuth} + /> + ); + await nextTick(); + const button = await container.findByTestId('dataSourceSelectableContextMenuHeaderLink'); + expect(button).toHaveTextContent('test2'); + }); + + it('should render warning if provide undefined dataSourceId', async () => { + const onSelectedDataSource = jest.fn(); + const container = render( + ds.attributes.auth.type !== AuthType.NoAuth} + /> + ); + await nextTick(); + const button = await container.findByTestId('dataSourceSelectableContextMenuHeaderLink'); + expect(button).toHaveTextContent(''); + expect(toasts.addWarning).toBeCalledWith('Data source with id: undefined is not available'); + }); + + it('should render warning if provide empty object', async () => { + const onSelectedDataSource = jest.fn(); + const container = render( + ds.attributes.auth.type !== AuthType.NoAuth} + /> + ); + await nextTick(); + const button = await container.findByTestId('dataSourceSelectableContextMenuHeaderLink'); + expect(button).toHaveTextContent(''); + expect(toasts.addWarning).toBeCalledWith('Data source with id: undefined is not available'); + }); + it('should warning if only provide label', async () => { + const onSelectedDataSource = jest.fn(); + const container = render( + ds.attributes.auth.type !== AuthType.NoAuth} + /> + ); + await nextTick(); + expect(toasts.addWarning).toBeCalledWith('Data source with id: undefined is not available'); + }); + it('should warning if only provide empty label', async () => { + const onSelectedDataSource = jest.fn(); + const container = render( + ds.attributes.auth.type !== AuthType.NoAuth} + /> + ); + await nextTick(); + expect(toasts.addWarning).toBeCalledWith('Data source with id: undefined is not available'); + }); + + it('should warning if only provide empty array', async () => { + const onSelectedDataSource = jest.fn(); + const container = render( + + ); + await nextTick(); + expect(toasts.addWarning).toBeCalledWith('Data source with id: undefined is not available'); + }); + + it('should render the selected option when pass in the valid dataSourceId', async () => { + const onSelectedDataSource = jest.fn(); + const container = mount( + + ); + await nextTick(); + const containerInstance = container.instance(); + expect(containerInstance.state).toEqual({ + dataSourceOptions: [ + { + id: 'test1', + label: 'test1', + }, + { + checked: 'on', + id: 'test2', + label: 'test2', + }, + { + id: 'test3', + label: 'test3', + }, + ], + defaultDataSource: null, + isPopoverOpen: false, + selectedOption: [ + { + id: 'test2', + label: 'test2', + }, + ], + }); + }); + + it('should render nothing when no default option or activeOption', async () => { + const onSelectedDataSource = jest.fn(); + spyOn(utils, 'getDefaultDataSource').and.returnValue(undefined); + const container = mount( + ds.attributes.auth.type !== AuthType.NoAuth} + /> + ); + await nextTick(); + + const containerInstance = container.instance(); + + expect(onSelectedDataSource).toBeCalledTimes(0); + expect(containerInstance.state).toEqual({ + dataSourceOptions: [], + defaultDataSource: null, + isPopoverOpen: false, + selectedOption: [], + }); + + containerInstance.onChange([{ id: 'test2', label: 'test2', checked: 'on' }]); + expect(containerInstance.state).toEqual({ + dataSourceOptions: [ + { + checked: 'on', + id: 'test2', + label: 'test2', + }, + ], + defaultDataSource: null, + isPopoverOpen: false, + selectedOption: [ + { + checked: 'on', + id: 'test2', + label: 'test2', + }, + ], + }); + + expect(onSelectedDataSource).toBeCalledWith([{ id: 'test2', label: 'test2' }]); + expect(onSelectedDataSource).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 5a4591597ab..88cee94cf6a 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 @@ -21,7 +21,7 @@ import { SavedObjectsClientContract, ToastsStart, } from 'opensearch-dashboards/public'; -import { getDataSourcesWithFields, getDefaultDataSource } from '../utils'; +import { getDataSourcesWithFields, getDefaultDataSource, getFilteredDataSources } from '../utils'; import { LocalCluster } from '../data_source_selector/data_source_selector'; import { SavedObject } from '../../../../../core/public'; import { DataSourceAttributes } from '../../types'; @@ -81,67 +81,106 @@ export class DataSourceSelectable extends React.Component< this.setState({ ...this.state, isPopoverOpen: false }); } + // Update the checked status of the selected data source. + getUpdatedDataSourceOptions( + selectedDataSourceId: string, + dataSourceOptions: DataSourceOption[] + ): SelectedDataSourceOption[] { + return dataSourceOptions.map((option) => ({ + ...option, + ...(option.id === selectedDataSourceId && { checked: 'on' }), + })); + } + + handleSelectedOption(dataSourceOptions: DataSourceOption[], defaultDataSource: string | null) { + const [{ id }] = this.props.selectedOption!; + const dsOption = dataSourceOptions.find((ds) => ds.id === id); + if (!dsOption) { + this.props.notifications.addWarning( + i18n.translate('dataSource.fetchDataSourceError', { + defaultMessage: `Data source with id: ${id} is not available`, + }) + ); + this.setState({ + ...this.state, + dataSourceOptions, + selectedOption: [], + defaultDataSource, + }); + this.props.onSelectedDataSources([]); + return; + } + const updatedDataSourceOptions: SelectedDataSourceOption[] = this.getUpdatedDataSourceOptions( + id, + dataSourceOptions + ); + this.setState({ + ...this.state, + dataSourceOptions: updatedDataSourceOptions, + selectedOption: [{ id, label: dsOption.label }], + defaultDataSource, + }); + this.props.onSelectedDataSources([{ id, label: dsOption.label }]); + } + + handleDefaultDataSource(dataSourceOptions: DataSourceOption[], defaultDataSource: string | null) { + const selectedDataSource = getDefaultDataSource( + dataSourceOptions, + LocalCluster, + defaultDataSource, + this.props.hideLocalCluster + ); + + // no active option, show warning + if (selectedDataSource.length === 0) { + this.props.notifications.addWarning('No connected data source available.'); + this.props.onSelectedDataSources([]); + return; + } + + const updatedDataSourceOptions: SelectedDataSourceOption[] = this.getUpdatedDataSourceOptions( + selectedDataSource[0].id, + dataSourceOptions + ); + + this.setState({ + ...this.state, + selectedOption: selectedDataSource, + dataSourceOptions: updatedDataSourceOptions, + defaultDataSource, + }); + + this.props.onSelectedDataSources(selectedDataSource); + } + async componentDidMount() { this._isMounted = true; - try { - let filteredDataSources: Array> = []; - let dataSourceOptions: DataSourceOption[] = []; - // Fetch data sources with fields + try { 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())); - } + const dataSourceOptions: DataSourceOption[] = getFilteredDataSources( + fetchedDataSources, + this.props.dataSourceFilter + ); - // 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); + if (this.props.selectedOption?.length) { + this.handleSelectedOption(dataSourceOptions, defaultDataSource); + return; } + + // handle default data source if there is no valid active option + this.handleDefaultDataSource(dataSourceOptions, defaultDataSource); } catch (error) { this.props.notifications.addWarning( i18n.translate('dataSource.fetchDataSourceError', { @@ -183,10 +222,9 @@ export class DataSourceSelectable extends React.Component< size="s" disabled={this.props.disabled || false} > - {(this.state.selectedOption && + {this.state.selectedOption && this.state.selectedOption.length > 0 && - this.state.selectedOption[0].label) || - ''} + this.state.selectedOption[0]?.label} ); diff --git a/src/plugins/data_source_management/public/components/data_source_selector/__snapshots__/data_source_selector.test.tsx.snap b/src/plugins/data_source_management/public/components/data_source_selector/__snapshots__/data_source_selector.test.tsx.snap index 495331156fe..5cb8738e5c1 100644 --- a/src/plugins/data_source_management/public/components/data_source_selector/__snapshots__/data_source_selector.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/data_source_selector/__snapshots__/data_source_selector.test.tsx.snap @@ -80,15 +80,15 @@ exports[`DataSourceSelector: check dataSource options should always place local }, Object { "id": "test1", - "label": "test1", + "label": "", }, Object { "id": "test2", - "label": "test2", + "label": "", }, Object { "id": "test3", - "label": "test3", + "label": "", }, ] } @@ -130,11 +130,11 @@ exports[`DataSourceSelector: check dataSource options should filter options if c }, Object { "id": "test2", - "label": "test2", + "label": "", }, Object { "id": "test3", - "label": "test3", + "label": "", }, ] } @@ -214,15 +214,15 @@ exports[`DataSourceSelector: check dataSource options should hide prepend if rem }, Object { "id": "test1", - "label": "test1", + "label": "", }, Object { "id": "test2", - "label": "test2", + "label": "", }, Object { "id": "test3", - "label": "test3", + "label": "", }, ] } @@ -325,15 +325,15 @@ exports[`DataSourceSelector: check dataSource options should show custom placeho }, Object { "id": "test1", - "label": "test1", + "label": "", }, Object { "id": "test2", - "label": "test2", + "label": "", }, Object { "id": "test3", - "label": "test3", + "label": "", }, ] } diff --git a/src/plugins/data_source_management/public/components/utils.test.ts b/src/plugins/data_source_management/public/components/utils.test.ts index 9dc3a8824cb..63fac96be57 100644 --- a/src/plugins/data_source_management/public/components/utils.test.ts +++ b/src/plugins/data_source_management/public/components/utils.test.ts @@ -31,6 +31,7 @@ import { mockUiSettingsCalls, getSingleDataSourceResponse, getDataSource, + getDataSourceOptions, } from '../mocks'; import { AuthType, @@ -518,24 +519,21 @@ describe('DataSourceManagement: Utils.ts', () => { const result = getFilteredDataSources(dataSources); - expect(result).toEqual(dataSources); + expect(result).toEqual([ + { + id: '1', + label: 'DataSource 1', + }, + ]); }); test('should return filtered data sources when a filter is provided', () => { const filter = (dataSource: SavedObject) => dataSource.id === '2'; const result = getFilteredDataSources(getDataSource, filter); - expect(result).toEqual([ { id: '2', - type: '', - references: [], - attributes: { - title: 'DataSource 2', - endpoint: '', - auth: { type: AuthType.NoAuth, credentials: undefined }, - name: AuthType.NoAuth, - }, + label: 'DataSource 2', }, ]); }); @@ -543,39 +541,34 @@ describe('DataSourceManagement: Utils.ts', () => { describe('getDefaultDataSource', () => { const LocalCluster = { id: 'local', label: 'Local Cluster' }; const hideLocalCluster = false; - const defaultOption = [{ id: '2', label: 'Default Option' }]; + const defaultOption = [{ id: '2', label: 'DataSource 2' }]; it('should return the default option if it exists in the data sources', () => { + mockUiSettingsCalls(uiSettings, 'get', '2'); const result = getDefaultDataSource( - getDataSource, + getDataSourceOptions, LocalCluster, - uiSettings, - hideLocalCluster, - defaultOption + '2', + hideLocalCluster ); expect(result).toEqual([defaultOption[0]]); }); it('should return local cluster if it exists and no default options in the data sources', () => { mockUiSettingsCalls(uiSettings, 'get', null); - const result = getDefaultDataSource( - getDataSource, - LocalCluster, - uiSettings, - hideLocalCluster - ); + const result = getDefaultDataSource(getDataSource, LocalCluster, null, hideLocalCluster); expect(result).toEqual([LocalCluster]); }); it('should return the default datasource if hideLocalCluster is false', () => { mockUiSettingsCalls(uiSettings, 'get', '2'); - const result = getDefaultDataSource(getDataSource, LocalCluster, uiSettings, true); + const result = getDefaultDataSource(getDataSourceOptions, LocalCluster, '2', false); expect(result).toEqual([{ id: '2', label: 'DataSource 2' }]); }); it('should return the first data source if no default option, hideLocalCluster is ture and no default datasource', () => { mockUiSettingsCalls(uiSettings, 'get', null); - const result = getDefaultDataSource(getDataSource, LocalCluster, uiSettings, true); + const result = getDefaultDataSource(getDataSourceOptions, LocalCluster, uiSettings, true); expect(result).toEqual([{ id: '1', label: 'DataSource 1' }]); }); }); diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index be004c98994..81c0cd5c707 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -81,52 +81,44 @@ export async function setFirstDataSourceAsDefault( export function getFilteredDataSources( dataSources: Array>, - filter?: (dataSource: SavedObject) => boolean -) { - return filter ? dataSources.filter((ds) => filter!(ds)) : dataSources; + filter = (ds: SavedObject) => true +): DataSourceOption[] { + return dataSources + .filter((ds) => filter!(ds)) + .map((ds) => ({ + id: ds.id, + label: ds.attributes?.title || '', + })) + .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())); } export function getDefaultDataSource( - dataSources: Array>, + dataSourcesOptions: DataSourceOption[], LocalCluster: DataSourceOption, - uiSettings?: IUiSettingsClient, - hideLocalCluster?: boolean, - defaultOption?: DataSourceOption[] + defaultDataSourceId: string | null, + hideLocalCluster?: boolean ) { - const defaultOptionId = defaultOption?.[0]?.id; - const defaultOptionDataSource = dataSources.find( - (dataSource) => dataSource.id === defaultOptionId - ); - - const defaultDataSourceId = uiSettings?.get('defaultDataSource', null) ?? null; - const defaultDataSourceAfterCheck = dataSources.find( + const defaultDataSourceAfterCheck = dataSourcesOptions.find( (dataSource) => dataSource.id === defaultDataSourceId ); - - if (defaultOptionDataSource) { - return [ - { - id: defaultOptionDataSource.id, - label: defaultOption?.[0]?.label || defaultOptionDataSource.attributes?.title, - }, - ]; - } if (defaultDataSourceAfterCheck) { return [ { id: defaultDataSourceAfterCheck.id, - label: defaultDataSourceAfterCheck.attributes?.title || '', + label: defaultDataSourceAfterCheck.label, }, ]; } + if (!hideLocalCluster) { return [LocalCluster]; } - if (dataSources.length > 0) { + + if (dataSourcesOptions.length > 0) { return [ { - id: dataSources[0].id, - label: dataSources[0].attributes.title, + id: dataSourcesOptions[0].id, + label: dataSourcesOptions[0].label, }, ]; } diff --git a/src/plugins/data_source_management/public/mocks.ts b/src/plugins/data_source_management/public/mocks.ts index f0b65d7343c..d6c89dc5c7d 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -115,6 +115,21 @@ export const getDataSource = [ }, ]; +export const getDataSourceOptions = [ + { + id: '1', + label: 'DataSource 1', + }, + { + id: '2', + label: 'DataSource 2', + }, + { + id: '3', + label: 'DataSource 1', + }, +]; + /* Mock data responses - JSON*/ export const getDataSourcesResponse = { savedObjects: [