From 5ef10da3f1be11b2f920c4abfa3c1982c500541c Mon Sep 17 00:00:00 2001 From: Xinrui Bai-amazon <139305463+xinruiba@users.noreply.github.com> Date: Mon, 4 Mar 2024 17:50:57 -0800 Subject: [PATCH] [Multi Data Source] Render credential form registered from AuthMethod (#6002) * [TokenExchange] Render credential form registered from AuthMethod Signed-off-by: Xinrui Bai * [UT] Add unittest to test registered credential form get rendered in create datasource page Signed-off-by: Xinrui Bai * [UT] Update test case descriptions Signed-off-by: Xinrui Bai * [Token Exchange] improve code format in create datasource page Signed-off-by: Xinrui Bai * [UT] Add unit test for edit datasource page Signed-off-by: Xinrui Bai * Update changelog file Signed-off-by: Xinrui Bai * update yml config file to original status Signed-off-by: Xinrui Bai * Resolving comments Signed-off-by: Xinrui Bai * [UT] Add more unit test to cover existing auth type and plugin registered Auth type scenario Signed-off-by: Xinrui Bai * Resolving comments, update pmport path Signed-off-by: Xinrui Bai --------- Signed-off-by: Xinrui Bai --- CHANGELOG.md | 1 + .../authentication_methods_registry.ts | 5 +- .../create_data_source_form.test.tsx | 157 +++++++++++++++++- .../create_form/create_data_source_form.tsx | 39 +++-- .../edit_form/edit_data_source_form.test.tsx | 43 +++++ .../edit_form/edit_data_source_form.tsx | 31 +++- 6 files changed, 259 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14fba16ff10a..f251407addf1 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] Hide/Show authentication method in multi data source plugin based on configuration ([#5916](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5916)) - [[Dynamic Configurations] Add support for dynamic application configurations ([#5855](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5855)) - [Workspace] Optional workspaces params in repository ([#5949](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5949)) +- [Multiple Datasource] Refactoring create and edit form to use authentication registry ([#6002](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6002)) ### 🐛 Bug Fixes diff --git a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts index 00c3b0dbf0ee..a4152cb0627a 100644 --- a/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts +++ b/src/plugins/data_source_management/public/auth_registry/authentication_methods_registry.ts @@ -9,7 +9,10 @@ import { EuiSuperSelectOption } from '@elastic/eui'; export interface AuthenticationMethod { name: string; credentialSourceOption: EuiSuperSelectOption; - credentialForm?: React.JSX.Element; + credentialForm?: ( + state: { [key: string]: any }, + setState: React.Dispatch> + ) => React.JSX.Element; crendentialFormField?: { [key: string]: string }; } diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx index 4f30ba9da5e4..1040a17584a0 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx @@ -17,7 +17,7 @@ import { sigV4AuthMethod, usernamePasswordAuthMethod, } from '../../../../types'; -import { AuthenticationMethodRegistery } from 'src/plugins/data_source_management/public/auth_registry'; +import { AuthenticationMethod, AuthenticationMethodRegistery } from '../../../../auth_registry'; const titleIdentifier = '[data-test-subj="createDataSourceFormTitleField"]'; const descriptionIdentifier = `[data-test-subj="createDataSourceFormDescriptionField"]`; @@ -363,3 +363,158 @@ describe('Datasource Management: Create Datasource form with different authType }); }); }); + +describe('Datasource Management: Create Datasource form with registered Auth Type', () => { + let component: ReactWrapper, React.Component<{}, {}, any>>; + const mockSubmitHandler = jest.fn(); + const mockTestConnectionHandler = jest.fn(); + const mockCancelHandler = jest.fn(); + + test('should call registered crendential form at the first round when registered method is at the first place and username & password disabled', () => { + const mockCredentialForm = jest.fn(); + const authTypeToBeTested = 'Some Auth Type'; + const authMethodToBeTested = { + name: authTypeToBeTested, + credentialSourceOption: { + value: authTypeToBeTested, + inputDisplay: 'some input', + }, + credentialForm: mockCredentialForm, + } as AuthenticationMethod; + + const authMethodCombinationsToBeTested = [ + [authMethodToBeTested], + [authMethodToBeTested, sigV4AuthMethod], + [authMethodToBeTested, noAuthCredentialAuthMethod], + [authMethodToBeTested, noAuthCredentialAuthMethod, sigV4AuthMethod], + ]; + + authMethodCombinationsToBeTested.forEach((authMethodCombination) => { + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery = new AuthenticationMethodRegistery(); + + authMethodCombination.forEach((authMethod) => { + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(authMethod); + }); + + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + + expect(mockCredentialForm).toHaveBeenCalled(); + }); + }); + + test('should not call registered crendential form at the first round when registered method is at the first place and username & password enabled', () => { + const mockCredentialForm = jest.fn(); + const authTypeToBeTested = 'Some Auth Type'; + const authMethodToBeTested = { + name: authTypeToBeTested, + credentialSourceOption: { + value: authTypeToBeTested, + inputDisplay: 'some input', + }, + credentialForm: mockCredentialForm, + } as AuthenticationMethod; + + const authMethodCombinationsToBeTested = [ + [authMethodToBeTested, usernamePasswordAuthMethod], + [authMethodToBeTested, usernamePasswordAuthMethod, sigV4AuthMethod], + [authMethodToBeTested, usernamePasswordAuthMethod, noAuthCredentialAuthMethod], + [ + authMethodToBeTested, + usernamePasswordAuthMethod, + noAuthCredentialAuthMethod, + sigV4AuthMethod, + ], + ]; + + authMethodCombinationsToBeTested.forEach((authMethodCombination) => { + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery = new AuthenticationMethodRegistery(); + + authMethodCombination.forEach((authMethod) => { + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(authMethod); + }); + + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + + expect(mockCredentialForm).not.toHaveBeenCalled(); + }); + }); + + test('should not call registered crendential form at the first round when registered method is not at the first place', () => { + const mockCredentialForm = jest.fn(); + const authTypeToBeTested = 'Some Auth Type'; + const authMethodToBeTested = { + name: authTypeToBeTested, + credentialSourceOption: { + value: authTypeToBeTested, + inputDisplay: 'some input', + }, + credentialForm: mockCredentialForm, + } as AuthenticationMethod; + + const authMethodCombinationsToBeTested = [ + [sigV4AuthMethod, authMethodToBeTested], + [noAuthCredentialAuthMethod, authMethodToBeTested], + [noAuthCredentialAuthMethod, authMethodToBeTested, sigV4AuthMethod], + ]; + + authMethodCombinationsToBeTested.forEach((authMethodCombination) => { + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery = new AuthenticationMethodRegistery(); + + authMethodCombination.forEach((authMethod) => { + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(authMethod); + }); + + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + + expect(mockCredentialForm).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx index e178ea1bcffa..25b082b8c6a1 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx @@ -22,6 +22,7 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; +import { AuthenticationMethodRegistery } from '../../../../auth_registry'; import { SigV4Content, SigV4ServiceName } from '../../../../../../data_source/common/data_sources'; import { AuthType, @@ -68,16 +69,17 @@ export class CreateDataSourceForm extends React.Component< authOptions: Array> = []; isNoAuthOptionEnabled: boolean; + authenticationMethodRegistery: AuthenticationMethodRegistery; constructor(props: CreateDataSourceProps, context: DataSourceManagementContextValue) { super(props, context); - const authenticationMethodRegistery = context.services.authenticationMethodRegistery; - const registeredAuthMethods = authenticationMethodRegistery.getAllAuthenticationMethods(); - const initialSelectedAuthMethod = getDefaultAuthMethod(authenticationMethodRegistery); + this.authenticationMethodRegistery = context.services.authenticationMethodRegistery; + const registeredAuthMethods = this.authenticationMethodRegistery.getAllAuthenticationMethods(); + const initialSelectedAuthMethod = getDefaultAuthMethod(this.authenticationMethodRegistery); this.isNoAuthOptionEnabled = - authenticationMethodRegistery.getAuthenticationMethod(AuthType.NoAuth) !== undefined; + this.authenticationMethodRegistery.getAuthenticationMethod(AuthType.NoAuth) !== undefined; this.authOptions = registeredAuthMethods.map((authMethod) => { return authMethod.credentialSourceOption; @@ -322,6 +324,23 @@ export class CreateDataSourceForm extends React.Component< }; }; + handleStateChange = (state: any) => { + this.setState(state); + }; + + getCredentialFormFromRegistry = (authType: string) => { + const registeredAuthMethod = this.authenticationMethodRegistery.getAuthenticationMethod( + authType + ); + const authCredentialForm = registeredAuthMethod?.credentialForm; + + if (authCredentialForm !== undefined) { + return authCredentialForm(this.state, this.handleStateChange); + } + + return null; + }; + /* Render methods */ /* Render header*/ @@ -362,6 +381,8 @@ export class CreateDataSourceForm extends React.Component< /* Render create new credentials*/ renderCreateNewCredentialsForm = (type: AuthType) => { switch (type) { + case AuthType.NoAuth: + return null; case AuthType.UsernamePasswordType: return ( <> @@ -498,7 +519,7 @@ export class CreateDataSourceForm extends React.Component< ); default: - break; + return this.getCredentialFormFromRegistry(type); } }; @@ -632,13 +653,7 @@ export class CreateDataSourceForm extends React.Component< {/* Create New credentials */} - {this.state.auth.type === AuthType.UsernamePasswordType - ? this.renderCreateNewCredentialsForm(this.state.auth.type) - : null} - - {this.state.auth.type === AuthType.SigV4 - ? this.renderCreateNewCredentialsForm(this.state.auth.type) - : null} + {this.renderCreateNewCredentialsForm(this.state.auth.type)} diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx index 4326d6e6832d..89d5c54cbc2a 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx @@ -21,6 +21,7 @@ import { sigV4AuthMethod, usernamePasswordAuthMethod, } from '../../../../types'; +import { AuthenticationMethod, AuthenticationMethodRegistery } from '../../../../auth_registry'; const titleFieldIdentifier = 'dataSourceTitle'; const titleFormRowIdentifier = '[data-test-subj="editDataSourceTitleFormRow"]'; @@ -340,3 +341,45 @@ describe('Datasource Management: Edit Datasource Form', () => { }); }); }); + +describe('With Registered Authentication', () => { + let component: ReactWrapper, React.Component<{}, {}, any>>; + const mockCredentialForm = jest.fn(); + + test('should call registered crendential form', () => { + const authTypeToBeTested = 'Some Auth Type'; + const authMethodToBeTest = { + name: authTypeToBeTested, + credentialSourceOption: { + value: authTypeToBeTested, + inputDisplay: 'some input', + }, + credentialForm: mockCredentialForm, + } as AuthenticationMethod; + + const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); + mockedContext.authenticationMethodRegistery = new AuthenticationMethodRegistery(); + mockedContext.authenticationMethodRegistery.registerAuthenticationMethod(authMethodToBeTest); + + component = mount( + wrapWithIntl( + + ), + { + wrappingComponent: OpenSearchDashboardsContextProvider, + wrappingComponentProps: { + services: mockedContext, + }, + } + ); + + expect(mockCredentialForm).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx index c3d7daa7db48..ee46af355312 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -24,6 +24,7 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; +import { AuthenticationMethodRegistery } from '../../../../auth_registry'; import { SigV4Content, SigV4ServiceName } from '../../../../../../data_source/common/data_sources'; import { Header } from '../header'; import { @@ -43,6 +44,7 @@ import { } from '../../../validation'; import { UpdatePasswordModal } from '../update_password_modal'; import { UpdateAwsCredentialModal } from '../update_aws_credential_modal'; +import { getDefaultAuthMethod } from '../../../utils'; export interface EditDataSourceProps { existingDataSource: DataSourceAttributes; @@ -72,23 +74,27 @@ export class EditDataSourceForm extends React.Component> = []; + authenticationMethodRegistery: AuthenticationMethodRegistery; constructor(props: EditDataSourceProps, context: DataSourceManagementContextValue) { super(props, context); - this.authOptions = context.services.authenticationMethodRegistery + this.authenticationMethodRegistery = context.services.authenticationMethodRegistery; + this.authOptions = this.authenticationMethodRegistery .getAllAuthenticationMethods() .map((authMethod) => { return authMethod.credentialSourceOption; }); + const initialSelectedAuthMethod = getDefaultAuthMethod(this.authenticationMethodRegistery); + this.state = { formErrorsByField: { ...defaultValidation }, title: '', description: '', endpoint: '', auth: { - type: AuthType.NoAuth, + type: initialSelectedAuthMethod?.name, credentials: { username: '', password: '', @@ -518,6 +524,23 @@ export class EditDataSourceForm extends React.Component { + this.setState(state); + }; + + getCredentialFormFromRegistry = (authType: string) => { + const registeredAuthMethod = this.authenticationMethodRegistery.getAuthenticationMethod( + authType + ); + const authCredentialForm = registeredAuthMethod?.credentialForm; + + if (authCredentialForm !== undefined) { + return authCredentialForm(this.state, this.handleStateChange); + } + + return null; + }; + /* Render methods */ /* Render modal for new credential */ @@ -796,12 +819,14 @@ export class EditDataSourceForm extends React.Component { switch (type) { + case AuthType.NoAuth: + return null; case AuthType.UsernamePasswordType: return this.renderUsernamePasswordFields(); case AuthType.SigV4: return this.renderSigV4ContentFields(); default: - return null; + return this.getCredentialFormFromRegistry(type); } };