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;