From 91a0530a508e96e12cacad09383a7e3e9606031f Mon Sep 17 00:00:00 2001 From: "Yuanqi(Ella) Zhu" <53279298+zhyuanqi@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:20:52 -0700 Subject: [PATCH] Make sure customer always have a default datasource (#6237) * Make sure customer always have a default datasource Signed-off-by: Yuanqi(Ella) Zhu * Handle set as default Signed-off-by: Yuanqi(Ella) Zhu --------- Signed-off-by: Yuanqi(Ella) Zhu Signed-off-by: Yuanqi(Ella) Zhu <53279298+zhyuanqi@users.noreply.github.com> Co-authored-by: Miki --- CHANGELOG.md | 1 + .../create_data_source_wizard.test.tsx | 3 +- .../create_data_source_wizard.tsx | 4 ++ .../data_source_table.test.tsx | 5 +- .../data_source_table/data_source_table.tsx | 22 +++++++- .../edit_data_source.test.tsx | 7 ++- .../edit_data_source/edit_data_source.tsx | 19 +++++++ .../public/components/utils.test.ts | 50 ++++++++++++++++++- .../public/components/utils.ts | 31 +++++++++++- .../data_source_management/public/mocks.ts | 24 +++++++++ 10 files changed, 160 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a85b38a2bfa7..ca581115e507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Refactor data source menu and interface to allow cleaner selection of component and related configurations ([#6256](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6256)) - [Multiple Datasource] Allow top nav menu to mount data source menu for use case when both menus are mounted ([#6268](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6268)) - [Workspace] Add create workspace page ([#6179](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6179)) +- [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)) diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx index 1fe6e4f5d499..aa4e128094c7 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx @@ -53,7 +53,7 @@ describe('Datasource Management: Create Datasource Wizard', () => { test('should create datasource successfully', async () => { spyOn(utils, 'createSingleDataSource').and.returnValue({}); - + spyOn(utils, 'handleSetDefaultDatasource').and.returnValue({}); await act(async () => { // @ts-ignore await component.find(formIdentifier).first().prop('handleSubmit')( @@ -62,6 +62,7 @@ describe('Datasource Management: Create Datasource Wizard', () => { }); expect(utils.createSingleDataSource).toHaveBeenCalled(); expect(history.push).toBeCalledWith(''); + expect(utils.handleSetDefaultDatasource).toHaveBeenCalled(); }); test('should fail to create datasource', async () => { diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx index 06b77efd9b94..58f9ae108083 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx @@ -21,6 +21,7 @@ import { getDataSources, testConnection, fetchDataSourceVersion, + handleSetDefaultDatasource, } from '../utils'; import { LoadingMask } from '../loading_mask'; @@ -35,6 +36,7 @@ export const CreateDataSourceWizard: React.FunctionComponent().services; /* State Variables */ @@ -76,6 +78,8 @@ export const CreateDataSourceWizard: React.FunctionComponent { it('should delete confirm modal confirm button work normally', async () => { spyOn(utils, 'deleteMultipleDataSources').and.returnValue(Promise.resolve({})); - + spyOn(utils, 'setFirstDataSourceAsDefault').and.returnValue({}); act(() => { // @ts-ignore component.find(tableIdentifier).props().selection.onSelectionChange(getMappedDataSources); @@ -143,10 +143,12 @@ describe('DataSourceTable', () => { }); component.update(); expect(component.find(confirmModalIdentifier).exists()).toBe(false); + expect(utils.setFirstDataSourceAsDefault).toHaveBeenCalled(); }); it('should delete datasources & fail', async () => { spyOn(utils, 'deleteMultipleDataSources').and.returnValue(Promise.reject({})); + spyOn(utils, 'setFirstDataSourceAsDefault').and.returnValue({}); act(() => { // @ts-ignore component.find(tableIdentifier).props().selection.onSelectionChange(getMappedDataSources); @@ -162,6 +164,7 @@ describe('DataSourceTable', () => { }); component.update(); expect(utils.deleteMultipleDataSources).toHaveBeenCalled(); + expect(utils.setFirstDataSourceAsDefault).not.toHaveBeenCalled(); // @ts-ignore expect(component.find(confirmModalIdentifier).exists()).toBe(false); }); diff --git a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx index b27f957fe142..9b9fd9488290 100644 --- a/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx +++ b/src/plugins/data_source_management/public/components/data_source_table/data_source_table.tsx @@ -29,7 +29,7 @@ import { } from '../../../../opensearch_dashboards_react/public'; import { DataSourceManagementContext, DataSourceTableItem, ToastMessageItem } from '../../types'; import { CreateButton } from '../create_button'; -import { deleteMultipleDataSources, getDataSources } from '../utils'; +import { deleteMultipleDataSources, getDataSources, setFirstDataSourceAsDefault } from '../utils'; import { LoadingMask } from '../loading_mask'; /* Table config */ @@ -232,6 +232,9 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { // Fetch data sources fetchDataSources(); setConfirmDeleteVisible(false); + // Check if default data source is deleted or not. + // if yes, then set the first existing datasource as default datasource. + setDefaultDataSource(); }) .catch(() => { handleDisplayToastMessage({ @@ -245,6 +248,23 @@ export const DataSourceTable = ({ history }: RouteComponentProps) => { }); }; + const setDefaultDataSource = async () => { + try { + for (const dataSource of selectedDataSources) { + if (uiSettings.get('defaultDataSource') === dataSource.id) { + await setFirstDataSourceAsDefault(savedObjects.client, uiSettings, true); + } + } + } catch (e) { + handleDisplayToastMessage({ + id: 'dataSourcesManagement.editDataSource.setDefaultDataSourceFailMsg', + defaultMessage: 'Unable to find a default datasource. Please set a new default datasource.', + }); + } finally { + setIsDeleting(false); + } + }; + /* Table selection handlers */ const onSelectionChange = (selected: DataSourceTableItem[]) => { setSelectedDataSources(selected); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx index 833886fff7f3..d8d175a920d9 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx @@ -138,7 +138,8 @@ describe('Datasource Management: Edit Datasource Wizard', () => { }); test('should delete datasource successfully', async () => { spyOn(utils, 'deleteDataSourceById').and.returnValue({}); - + spyOn(utils, 'setFirstDataSourceAsDefault').and.returnValue({}); + spyOn(uiSettings, 'get').and.returnValue('test1'); await act(async () => { // @ts-ignore await component.find(formIdentifier).first().prop('onDeleteDataSource')( @@ -147,9 +148,12 @@ describe('Datasource Management: Edit Datasource Wizard', () => { }); expect(utils.deleteDataSourceById).toHaveBeenCalled(); expect(history.push).toBeCalledWith(''); + expect(utils.setFirstDataSourceAsDefault).toHaveBeenCalled(); }); test('should fail to delete datasource', async () => { spyOn(utils, 'deleteDataSourceById').and.throwError('error'); + spyOn(utils, 'setFirstDataSourceAsDefault').and.returnValue({}); + spyOn(uiSettings, 'get').and.returnValue('test1'); await act(async () => { // @ts-ignore await component.find(formIdentifier).first().prop('onDeleteDataSource')( @@ -158,6 +162,7 @@ describe('Datasource Management: Edit Datasource Wizard', () => { }); component.update(); expect(utils.deleteDataSourceById).toHaveBeenCalled(); + expect(utils.setFirstDataSourceAsDefault).not.toHaveBeenCalled(); }); test('should test connection', () => { spyOn(utils, 'testConnection'); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx index 46e253b2b85b..ab1e8531c801 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx @@ -17,6 +17,7 @@ import { getDataSources, testConnection, updateDataSourceById, + setFirstDataSourceAsDefault, } from '../utils'; import { getEditBreadcrumbs } from '../breadcrumbs'; import { EditDataSourceForm } from './components/edit_form/edit_data_source_form'; @@ -109,6 +110,10 @@ export const EditDataSource: React.FunctionComponent { + try { + if (uiSettings.get('defaultDataSource') === dataSourceID) { + await setFirstDataSourceAsDefault(savedObjects.client, uiSettings, true); + } + } catch (e) { + setIsLoading(false); + handleDisplayToastMessage({ + id: 'dataSourcesManagement.editDataSource.setDefaultDataSourceFailMsg', + defaultMessage: 'Unable to find a default datasource. Please set a new default datasource.', + }); + } + }; + /* Handle Test connection */ const handleTestConnection = async (attributes: DataSourceAttributes) => { await testConnection(http, attributes, dataSourceID); 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 a4069f01907b..f2b1f709cb18 100644 --- a/src/plugins/data_source_management/public/components/utils.test.ts +++ b/src/plugins/data_source_management/public/components/utils.test.ts @@ -14,6 +14,8 @@ import { isValidUrl, testConnection, updateDataSourceById, + handleSetDefaultDatasource, + setFirstDataSourceAsDefault, } from './utils'; import { coreMock } from '../../../../core/public/mocks'; import { @@ -24,6 +26,8 @@ import { mockDataSourceAttributesWithAuth, mockErrorResponseForSavedObjectsCalls, mockResponseForSavedObjectsCalls, + mockUiSettingsCalls, + getSingleDataSourceResponse, } from '../mocks'; import { AuthType, @@ -36,6 +40,7 @@ import { AuthenticationMethod, AuthenticationMethodRegistry } from '../auth_regi import { deepEqual } from 'assert'; const { savedObjects } = coreMock.createStart(); +const { uiSettings } = coreMock.createStart(); describe('DataSourceManagement: Utils.ts', () => { describe('Get data source', () => { @@ -274,7 +279,50 @@ describe('DataSourceManagement: Utils.ts', () => { expect(getDefaultAuthMethod(authenticationMethodRegistry)?.name).toBe(AuthType.NoAuth); }); }); - + describe('handle set default datasource', () => { + beforeEach(() => { + jest.clearAllMocks(); // Reset all mock calls before each test + }); + test('should set default datasource when it does not have default datasource ', async () => { + mockUiSettingsCalls(uiSettings, 'get', null); + mockResponseForSavedObjectsCalls(savedObjects.client, 'find', getDataSourcesResponse); + await handleSetDefaultDatasource(savedObjects.client, uiSettings); + expect(uiSettings.set).toHaveBeenCalled(); + }); + test('should not set default datasource when it has default datasouce', async () => { + mockUiSettingsCalls(uiSettings, 'get', 'test'); + mockResponseForSavedObjectsCalls(savedObjects.client, 'find', getDataSourcesResponse); + await handleSetDefaultDatasource(savedObjects.client, uiSettings); + expect(uiSettings.set).not.toHaveBeenCalled(); + }); + }); + describe('set first aataSource as default', () => { + beforeEach(() => { + jest.clearAllMocks(); // Reset all mock calls before each test + }); + test('should set defaultDataSource if more than one data source exists', async () => { + mockResponseForSavedObjectsCalls(savedObjects.client, 'find', getDataSourcesResponse); + await setFirstDataSourceAsDefault(savedObjects.client, uiSettings, true); + expect(uiSettings.set).toHaveBeenCalled(); + }); + test('should set defaultDataSource if only one data source exists', async () => { + mockResponseForSavedObjectsCalls(savedObjects.client, 'find', getSingleDataSourceResponse); + await setFirstDataSourceAsDefault(savedObjects.client, uiSettings, true); + expect(uiSettings.set).toHaveBeenCalled(); + }); + test('should not set defaultDataSource if no data source exists', async () => { + mockResponseForSavedObjectsCalls(savedObjects.client, 'find', { savedObjects: [] }); + await setFirstDataSourceAsDefault(savedObjects.client, uiSettings, true); + expect(uiSettings.remove).toHaveBeenCalled(); + expect(uiSettings.set).not.toHaveBeenCalled(); + }); + test('should not set defaultDataSource if no data source exists and no default datasouce', async () => { + mockResponseForSavedObjectsCalls(savedObjects.client, 'find', { savedObjects: [] }); + await setFirstDataSourceAsDefault(savedObjects.client, uiSettings, false); + expect(uiSettings.remove).not.toHaveBeenCalled(); + expect(uiSettings.set).not.toHaveBeenCalled(); + }); + }); describe('Check extractRegisteredAuthTypeCredentials method', () => { test('Should extract credential field successfully', () => { const authTypeToBeTested = 'Some Auth Type'; diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index d7d6e94265e2..b911203cd288 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -3,7 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpStart, SavedObjectsClientContract, SavedObject } from 'src/core/public'; +import { + HttpStart, + SavedObjectsClientContract, + SavedObject, + IUiSettingsClient, +} from 'src/core/public'; import { DataSourceAttributes, DataSourceTableItem, @@ -49,6 +54,30 @@ export async function getDataSourcesWithFields( return response?.savedObjects; } +export async function handleSetDefaultDatasource( + savedObjectsClient: SavedObjectsClientContract, + uiSettings: IUiSettingsClient +) { + if (uiSettings.get('defaultDataSource', null) === null) { + return await setFirstDataSourceAsDefault(savedObjectsClient, uiSettings, false); + } +} + +export async function setFirstDataSourceAsDefault( + savedObjectsClient: SavedObjectsClientContract, + uiSettings: IUiSettingsClient, + exists: boolean +) { + if (exists) { + uiSettings.remove('defaultDataSource'); + } + const listOfDataSources: DataSourceTableItem[] = await getDataSources(savedObjectsClient); + if (Array.isArray(listOfDataSources) && listOfDataSources.length >= 1) { + const datasourceId = listOfDataSources[0].id; + return await uiSettings.set('defaultDataSource', datasourceId); + } +} + 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 90368762698f..d04fc2a362d3 100644 --- a/src/plugins/data_source_management/public/mocks.ts +++ b/src/plugins/data_source_management/public/mocks.ts @@ -6,6 +6,7 @@ import React from 'react'; import { throwError } from 'rxjs'; import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; +import { IUiSettingsClient } from 'src/core/public'; import { AuthType, DataSourceAttributes } from './types'; import { coreMock } from '../../../core/public/mocks'; import { @@ -62,6 +63,21 @@ export const mockManagementPlugin = { docLinks, }; +export const getSingleDataSourceResponse = { + savedObjects: [ + { + id: 'test', + type: 'data-source', + description: 'test datasource', + title: 'test', + get(field: string) { + const me: any = this || {}; + return me[field]; + }, + }, + ], +}; + /* Mock data responses - JSON*/ export const getDataSourcesResponse = { savedObjects: [ @@ -263,6 +279,14 @@ export const mockErrorResponseForSavedObjectsCalls = ( ); }; +export const mockUiSettingsCalls = ( + uiSettings: IUiSettingsClient, + uiSettingsMethodName: 'get' | 'set', + response: any +) => { + (uiSettings[uiSettingsMethodName] as jest.Mock).mockReturnValue(response); +}; + export interface TestPluginReturn { setup: DataSourceManagementPluginSetup; doStart: () => DataSourceManagementPluginStart;