diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b64758f4ca7..f5b9cf279694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Make sure customer always have a default datasource ([#6237](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6237)) - [Workspace] Add workspace list page ([#6182](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6182)) - [Workspace] Add workspaces column to saved objects page ([#6225](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6225)) +- [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) - [Workspace] Add permission control logic ([#6052](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6052)) 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 7306d202ff7b..495331156fee 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 @@ -13,6 +13,7 @@ exports[`DataSourceSelector should render normally with local cluster is hidden options={Array []} placeholder="Select a data source" prepend="Data source" + renderOption={[Function]} selectedOptions={Array []} singleSelection={ Object { @@ -43,6 +44,7 @@ exports[`DataSourceSelector should render normally with local cluster not hidden } placeholder="Select a data source" prepend="Data source" + renderOption={[Function]} selectedOptions={ Array [ Object { @@ -92,6 +94,7 @@ exports[`DataSourceSelector: check dataSource options should always place local } placeholder="Select a data source" prepend="Data source" + renderOption={[Function]} selectedOptions={ Array [ Object { @@ -137,6 +140,45 @@ exports[`DataSourceSelector: check dataSource options should filter options if c } placeholder="Select a data source" prepend="Data source" + renderOption={[Function]} + selectedOptions={ + Array [ + Object { + "id": "", + "label": "Local cluster", + }, + ] + } + singleSelection={ + Object { + "asPlainText": true, + } + } + sortMatchesBy="none" +/> +`; + +exports[`DataSourceSelector: check dataSource options should get default datasource if uiSettings exists 1`] = ` + +`; + +exports[`DataSourceSelector: check dataSource options should not render options with default badge when id does not matches defaultDataSource 1`] = ` + { let client: SavedObjectsClientContract; + const { uiSettings } = coreMock.createSetup(); const { toasts } = notificationServiceMock.createStartContract(); beforeEach(() => { @@ -27,7 +29,7 @@ describe('create data source selector', () => { hideLocalCluster: false, fullWidth: false, }; - const TestComponent = createDataSourceSelector(); + const TestComponent = createDataSourceSelector(uiSettings); const component = render(); expect(component).toMatchSnapshot(); expect(client.find).toBeCalledWith({ diff --git a/src/plugins/data_source_management/public/components/data_source_selector/create_data_source_selector.tsx b/src/plugins/data_source_management/public/components/data_source_selector/create_data_source_selector.tsx index ff6b7503a0bb..485d192668a5 100644 --- a/src/plugins/data_source_management/public/components/data_source_selector/create_data_source_selector.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selector/create_data_source_selector.tsx @@ -4,8 +4,11 @@ */ import React from 'react'; +import { IUiSettingsClient } from 'src/core/public'; import { DataSourceSelector, DataSourceSelectorProps } from './data_source_selector'; -export function createDataSourceSelector() { - return (props: DataSourceSelectorProps) => ; +export function createDataSourceSelector(uiSettings: IUiSettingsClient) { + return (props: DataSourceSelectorProps) => ( + + ); } diff --git a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.test.tsx b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.test.tsx index 86eb892e8cd8..d1203584d4b5 100644 --- a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.test.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.test.tsx @@ -8,8 +8,13 @@ import { DataSourceSelector } from './data_source_selector'; import { SavedObjectsClientContract } from '../../../../../core/public'; import { notificationServiceMock } from '../../../../../core/public/mocks'; import React from 'react'; -import { getDataSourcesWithFieldsResponse, mockResponseForSavedObjectsCalls } from '../../mocks'; +import { + getDataSourcesWithFieldsResponse, + mockManagementPlugin, + mockResponseForSavedObjectsCalls, +} from '../../mocks'; import { AuthType } from 'src/plugins/data_source/common/data_sources'; +import * as utils from '../utils'; describe('DataSourceSelector', () => { let component: ShallowWrapper, React.Component<{}, {}, any>>; @@ -69,8 +74,11 @@ describe('DataSourceSelector: check dataSource options', () => { let client: SavedObjectsClientContract; const { toasts } = notificationServiceMock.createStartContract(); const nextTick = () => new Promise((res) => process.nextTick(res)); + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + const uiSettings = mockedContext.uiSettings; beforeEach(async () => { + jest.clearAllMocks(); client = { find: jest.fn().mockResolvedValue([]), } as any; @@ -168,6 +176,47 @@ describe('DataSourceSelector: check dataSource options', () => { component.instance().componentDidMount!(); await nextTick(); expect(component).toMatchSnapshot(); - expect(toasts.addWarning).toBeCalledTimes(0); + expect(toasts.addWarning).toHaveBeenCalled(); + }); + + it('should get default datasource if uiSettings exists', async () => { + spyOn(uiSettings, 'get').and.returnValue('test1'); + spyOn(utils, 'getFilteredDataSources').and.returnValue([]); + spyOn(utils, 'getDefaultDataSource').and.returnValue([]); + component = shallow( + + ); + + component.instance().componentDidMount!(); + await nextTick(); + expect(component).toMatchSnapshot(); + expect(uiSettings.get).toBeCalledWith('defaultDataSource', null); + expect(utils.getFilteredDataSources).toHaveBeenCalled(); + expect(utils.getDefaultDataSource).toHaveBeenCalled(); + expect(toasts.addWarning).toHaveBeenCalled(); + }); + + it('should not render options with default badge when id does not matches defaultDataSource', () => { + component = shallow( + + ); + expect(component).toMatchSnapshot(); + expect(component.find('EuiComboBox').exists()).toBe(true); }); }); diff --git a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx index 3e9f4c377160..1f4782c8b896 100644 --- a/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx +++ b/src/plugins/data_source_management/public/components/data_source_selector/data_source_selector.tsx @@ -5,9 +5,10 @@ import React from 'react'; import { i18n } from '@osd/i18n'; -import { EuiComboBox } from '@elastic/eui'; +import { EuiComboBox, EuiBadge, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { SavedObjectsClientContract, ToastsStart, SavedObject } from 'opensearch-dashboards/public'; -import { getDataSourcesWithFields } from '../utils'; +import { IUiSettingsClient } from 'src/core/public'; +import { getDataSourcesWithFields, getDefaultDataSource, getFilteredDataSources } from '../utils'; import { DataSourceAttributes } from '../../types'; export const LocalCluster: DataSourceOption = { @@ -29,11 +30,13 @@ export interface DataSourceSelectorProps { removePrepend?: boolean; dataSourceFilter?: (dataSource: SavedObject) => boolean; compressed?: boolean; + uiSettings?: IUiSettingsClient; } interface DataSourceSelectorState { selectedOption: DataSourceOption[]; allDataSources: Array>; + defaultDataSource: string | null; } export interface DataSourceOption { @@ -53,6 +56,7 @@ export class DataSourceSelector extends React.Component< this.state = { allDataSources: [], + defaultDataSource: '', selectedOption: this.props.defaultOption ? this.props.defaultOption : this.props.hideLocalCluster @@ -67,6 +71,13 @@ export class DataSourceSelector extends React.Component< async componentDidMount() { this._isMounted = true; + + const currentDefaultDataSource = this.props.uiSettings?.get('defaultDataSource', null) ?? null; + this.setState({ + ...this.state, + defaultDataSource: currentDefaultDataSource, + }); + getDataSourcesWithFields(this.props.savedObjectsClient, ['id', 'title', 'auth.type']) .then((fetchedDataSources) => { if (fetchedDataSources?.length) { @@ -76,6 +87,25 @@ export class DataSourceSelector extends React.Component< allDataSources: fetchedDataSources, }); } + const dataSources = getFilteredDataSources( + this.state.allDataSources, + this.props.dataSourceFilter + ); + const selectedDataSource = getDefaultDataSource( + dataSources, + LocalCluster, + this.props.uiSettings, + this.props.hideLocalCluster, + this.props.defaultOption + ); + if (selectedDataSource.length === 0) { + this.props.notifications.addWarning('No connected data source available.'); + } else { + this.props.onSelectedDataSource(selectedDataSource); + this.setState({ + selectedOption: selectedDataSource, + }); + } }) .catch(() => { this.props.notifications.addWarning( @@ -100,9 +130,11 @@ export class DataSourceSelector extends React.Component< ? 'Select a data source' : this.props.placeholderText; - const dataSources = this.props.dataSourceFilter - ? this.state.allDataSources.filter((ds) => this.props.dataSourceFilter!(ds)) - : this.state.allDataSources; + // The filter condition can be changed, thus we filter again here to make sure each time we will get the filtered data sources before rendering + const dataSources = getFilteredDataSources( + this.state.allDataSources, + this.props.dataSourceFilter + ); const options = dataSources.map((ds) => ({ id: ds.id, label: ds.attributes?.title || '' })); if (!this.props.hideLocalCluster) { @@ -140,6 +172,16 @@ export class DataSourceSelector extends React.Component< isDisabled={this.props.disabled} fullWidth={this.props.fullWidth || false} data-test-subj={'dataSourceSelectorComboBox'} + renderOption={(option) => ( + + {option.label} + {option.id === this.state.defaultDataSource && ( + + Default + + )} + + )} /> ); } 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 f2b1f709cb18..9dc3a8824cb6 100644 --- a/src/plugins/data_source_management/public/components/utils.test.ts +++ b/src/plugins/data_source_management/public/components/utils.test.ts @@ -16,6 +16,8 @@ import { updateDataSourceById, handleSetDefaultDatasource, setFirstDataSourceAsDefault, + getFilteredDataSources, + getDefaultDataSource, } from './utils'; import { coreMock } from '../../../../core/public/mocks'; import { @@ -28,6 +30,7 @@ import { mockResponseForSavedObjectsCalls, mockUiSettingsCalls, getSingleDataSourceResponse, + getDataSource, } from '../mocks'; import { AuthType, @@ -35,9 +38,10 @@ import { sigV4AuthMethod, usernamePasswordAuthMethod, } from '../types'; -import { HttpStart } from 'opensearch-dashboards/public'; +import { HttpStart, SavedObject } from 'opensearch-dashboards/public'; import { AuthenticationMethod, AuthenticationMethodRegistry } from '../auth_registry'; import { deepEqual } from 'assert'; +import { DataSourceAttributes } from 'src/plugins/data_source/common/data_sources'; const { savedObjects } = coreMock.createStart(); const { uiSettings } = coreMock.createStart(); @@ -495,4 +499,84 @@ describe('DataSourceManagement: Utils.ts', () => { expect(deepEqual(registedAuthTypeCredentials, expectExtractedAuthCredentials)); }); }); + + describe('Check on get filter datasource', () => { + test('should return all data sources when no filter is provided', () => { + const dataSources: Array> = [ + { + id: '1', + type: '', + references: [], + attributes: { + title: 'DataSource 1', + endpoint: '', + auth: { type: AuthType.NoAuth, credentials: undefined }, + name: AuthType.NoAuth, + }, + }, + ]; + + const result = getFilteredDataSources(dataSources); + + expect(result).toEqual(dataSources); + }); + + 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, + }, + }, + ]); + }); + }); + describe('getDefaultDataSource', () => { + const LocalCluster = { id: 'local', label: 'Local Cluster' }; + const hideLocalCluster = false; + const defaultOption = [{ id: '2', label: 'Default Option' }]; + + it('should return the default option if it exists in the data sources', () => { + const result = getDefaultDataSource( + getDataSource, + LocalCluster, + uiSettings, + hideLocalCluster, + defaultOption + ); + 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 + ); + 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); + 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); + 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 b911203cd288..50960936e222 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -16,6 +16,7 @@ import { noAuthCredentialAuthMethod, } from '../types'; import { AuthenticationMethodRegistry } from '../auth_registry'; +import { DataSourceOption } from './data_source_selector/data_source_selector'; export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient @@ -78,6 +79,60 @@ export async function setFirstDataSourceAsDefault( } } +export function getFilteredDataSources( + dataSources: Array>, + filter?: (dataSource: SavedObject) => boolean +) { + return filter ? dataSources.filter((ds) => filter!(ds)) : dataSources; +} + +export function getDefaultDataSource( + dataSources: Array>, + LocalCluster: DataSourceOption, + uiSettings?: IUiSettingsClient, + hideLocalCluster?: boolean, + defaultOption?: DataSourceOption[] +) { + const defaultOptionId = defaultOption?.[0]?.id; + const defaultOptionDataSource = dataSources.find( + (dataSource) => dataSource.id === defaultOptionId + ); + + const defaultDataSourceId = uiSettings?.get('defaultDataSource', null) ?? null; + const defaultDataSourceAfterCheck = dataSources.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 || '', + }, + ]; + } + if (!hideLocalCluster) { + return [LocalCluster]; + } + if (dataSources.length > 0) { + return [ + { + id: dataSources[0].id, + label: dataSources[0].attributes.title, + }, + ]; + } + return []; +} + export async function getDataSourceById( id: string, savedObjectsClient: SavedObjectsClientContract diff --git a/src/plugins/data_source_management/public/mocks.ts b/src/plugins/data_source_management/public/mocks.ts index 13ccbf4e8f26..257ea956ec5d 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -78,6 +78,42 @@ export const getSingleDataSourceResponse = { ], }; +export const getDataSource = [ + { + id: '1', + type: '', + references: [], + attributes: { + title: 'DataSource 1', + endpoint: '', + auth: { type: AuthType.NoAuth, credentials: undefined }, + name: AuthType.NoAuth, + }, + }, + { + id: '2', + type: '', + references: [], + attributes: { + title: 'DataSource 2', + endpoint: '', + auth: { type: AuthType.NoAuth, credentials: undefined }, + name: AuthType.NoAuth, + }, + }, + { + id: '3', + type: '', + references: [], + attributes: { + title: 'DataSource 1', + endpoint: '', + auth: { type: AuthType.NoAuth, credentials: undefined }, + name: AuthType.NoAuth, + }, + }, +]; + /* Mock data responses - JSON*/ export const getDataSourcesResponse = { savedObjects: [ diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index 9e6da39dc08b..c6a978ae7b61 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -57,6 +57,7 @@ export class DataSourceManagementPlugin { management, indexPatternManagement, dataSource }: DataSourceManagementSetupDependencies ) { const opensearchDashboardsSection = management.sections.section.opensearchDashboards; + const uiSettings = core.uiSettings; if (!opensearchDashboardsSection) { throw new Error('`opensearchDashboards` management section not found.'); @@ -102,7 +103,7 @@ export class DataSourceManagementPlugin return { registerAuthenticationMethod, ui: { - DataSourceSelector: createDataSourceSelector(), + DataSourceSelector: createDataSourceSelector(uiSettings), getDataSourceMenu: () => createDataSourceMenu(), }, }; diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index 3d3655b3bd64..ebd70960ba1e 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -39,6 +39,7 @@ import { ApplicationStart, ChromeStart, CoreStart, + IUiSettingsClient, NotificationsStart, SavedObjectsStart, ScopedHistory, @@ -48,7 +49,6 @@ import { DataSourceSelector } from '../../data_source_management/public'; import { DevToolApp } from './dev_tool'; import { DevToolsSetupDependencies } from './plugin'; import { addHelpMenuToAppChrome } from './utils/util'; - interface DevToolsWrapperProps { devTools: readonly DevToolApp[]; activeDevTool: DevToolApp; @@ -57,6 +57,7 @@ interface DevToolsWrapperProps { notifications: NotificationsStart; dataSourceEnabled: boolean; hideLocalCluster: boolean; + uiSettings: IUiSettingsClient; } interface MountedDevToolDescriptor { @@ -73,8 +74,10 @@ function DevToolsWrapper({ notifications: { toasts }, dataSourceEnabled, hideLocalCluster, + uiSettings, }: DevToolsWrapperProps) { const mountedTool = useRef(null); + const [isLoading, setIsLoading] = React.useState(true); useEffect( () => () => { @@ -111,6 +114,7 @@ function DevToolsWrapper({ mountpoint: mountPoint, unmountHandler, }; + setIsLoading(false); }; return ( @@ -131,7 +135,7 @@ function DevToolsWrapper({ ))} - {dataSourceEnabled ? ( + {dataSourceEnabled && !isLoading ? (
) : null} @@ -213,7 +218,7 @@ function setBreadcrumbs(chrome: ChromeStart) { } export function renderApp( - { application, chrome, docLinks, savedObjects, notifications }: CoreStart, + { application, chrome, docLinks, savedObjects, notifications, uiSettings }: CoreStart, element: HTMLElement, history: ScopedHistory, devTools: readonly DevToolApp[], @@ -251,6 +256,7 @@ export function renderApp( notifications={notifications} dataSourceEnabled={dataSourceEnabled} hideLocalCluster={hideLocalCluster} + uiSettings={uiSettings} /> )} /> diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index cb13d06bfb18..a36f231b38ca 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -236,6 +236,7 @@ class TutorialDirectoryUi extends React.Component { onSelectedDataSource={this.onSelectedDataSourceChange} disabled={!isDataSourceEnabled} hideLocalCluster={isLocalClusterHidden} + uiSettings={getServices().uiSettings} /> ) : null;