diff --git a/webapp/packages/core-connections/src/ConnectionAuthService.ts b/webapp/packages/core-connections/src/ConnectionAuthService.ts index 35261367ee..01d0fd2c79 100644 --- a/webapp/packages/core-connections/src/ConnectionAuthService.ts +++ b/webapp/packages/core-connections/src/ConnectionAuthService.ts @@ -55,11 +55,18 @@ export class ConnectionAuthService { } const sshConfig = connection.networkHandlersConfig.find(state => state.id === SSH_TUNNEL_ID); + const sshAuthRequired = sshConfig?.enabled && !sshConfig?.savePassword; + const networkHandlers: string[] = []; - const isSSHAuthNeeded = sshConfig?.enabled && !sshConfig?.savePassword; + if (sshAuthRequired) { + networkHandlers.push(SSH_TUNNEL_ID); + } - if (connection.authNeeded || isSSHAuthNeeded) { - await this.commonDialogService.open(DatabaseAuthDialog, connectionId); + if (connection.authNeeded || sshAuthRequired) { + await this.commonDialogService.open(DatabaseAuthDialog, { + connectionId, + networkHandlers, + }); } else { await this.connectionInfoResource.init({ id: connectionId }); } diff --git a/webapp/packages/core-connections/src/ConnectionCredentials/ConnectionCredentialsDialog.tsx b/webapp/packages/core-connections/src/ConnectionCredentials/ConnectionCredentialsDialog.tsx new file mode 100644 index 0000000000..bbe71f127f --- /dev/null +++ b/webapp/packages/core-connections/src/ConnectionCredentials/ConnectionCredentialsDialog.tsx @@ -0,0 +1,79 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import styled, { css } from 'reshadow'; + +import { SubmittingForm, useFocus, Button } from '@cloudbeaver/core-blocks'; +import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import { useDBDriver } from '../useDBDriver'; +import { ConnectionCredentialsForm } from './ConnectionCredentialsForm'; + +const styles = css` + CommonDialogWrapper { + min-height: 400px; + min-width: 500px; + } + SubmittingForm { + overflow: auto; + margin: auto; + flex: 1; + display: flex; + flex-direction: column; + } + ConnectionCredentialsForm { + align-content: center; + } + Button { + margin-left: auto; + } +`; + +interface Payload { + config: ConnectionConfig; + authModelId: string | null; + networkHandlers?: string[]; + driverId?: string; +} + +export const ConnectionCredentialsDialog = observer(function ConnectionCredentialsDialog({ + payload, + rejectDialog, + resolveDialog, +}: DialogComponentProps) { + const translate = useTranslate(); + const [focusedRef] = useFocus({ focusFirstChild: true }); + const { driver } = useDBDriver(payload.driverId || ''); + + return styled(useStyles(styles))( + resolveDialog()}> + {translate('ui_apply')} + + )} + onReject={rejectDialog} + > + resolveDialog()}> + + + + ); +}); diff --git a/webapp/packages/core-connections/src/ConnectionCredentials/ConnectionCredentialsForm.tsx b/webapp/packages/core-connections/src/ConnectionCredentials/ConnectionCredentialsForm.tsx new file mode 100644 index 0000000000..09d54dfcae --- /dev/null +++ b/webapp/packages/core-connections/src/ConnectionCredentials/ConnectionCredentialsForm.tsx @@ -0,0 +1,74 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import styled from 'reshadow'; + +import { BASE_CONTAINERS_STYLES, Container, FieldCheckboxNew, Group, GroupTitle, ObjectPropertyInfoFormNew, TextPlaceholder, useMapResource } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import { DatabaseAuthModelsResource } from '../DatabaseAuthModelsResource'; +import type { IConnectionAuthCredentialsConfig } from './IConnectionAuthCredentialsConfig'; +import { NetworkHandlers } from './NetworkHandlers'; + +interface Props { + config: Partial; + authModelId: string | null; + networkHandlers?: string[]; + formId?: string; + allowSaveCredentials?: boolean; + disabled?: boolean; + className?: string; +} + +export const ConnectionCredentialsForm: React.FC = observer(function ConnectionCredentialsForm({ + config, networkHandlers, authModelId, formId, allowSaveCredentials, disabled, className, +}) { + const translate = useTranslate(); + const { data: authModel } = useMapResource(DatabaseAuthModelsResource, authModelId); + + return styled(useStyles(BASE_CONTAINERS_STYLES))( + + {authModel && ( + + {authModel.properties ? ( + <> + {!!networkHandlers?.length && {translate('connections_database_authentication')}} + + {allowSaveCredentials && ( + + )} + + ) : ( + Authentication data is not avaliable + )} + + )} + {networkHandlers && config.networkHandlersConfig && ( + + )} + + ); +}); diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/IFormInitConfig.ts b/webapp/packages/core-connections/src/ConnectionCredentials/IConnectionAuthCredentialsConfig.ts similarity index 58% rename from webapp/packages/core-connections/src/DatabaseAuthDialog/IFormInitConfig.ts rename to webapp/packages/core-connections/src/ConnectionCredentials/IConnectionAuthCredentialsConfig.ts index 6bfc600883..3e4c776ebb 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/IFormInitConfig.ts +++ b/webapp/packages/core-connections/src/ConnectionCredentials/IConnectionAuthCredentialsConfig.ts @@ -8,8 +8,8 @@ import type { NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; -import type { ConnectionInitConfig } from '../ConnectionInfoResource'; - -export interface IFormInitConfig extends Omit, 'id'> { - networkCredentials: NetworkHandlerConfigInput[]; +export interface IConnectionAuthCredentialsConfig { + credentials: any; + networkHandlersConfig: NetworkHandlerConfigInput[]; + saveCredentials: boolean | undefined; } diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx b/webapp/packages/core-connections/src/ConnectionCredentials/NetworkHandlerAuthForm.tsx similarity index 56% rename from webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx rename to webapp/packages/core-connections/src/ConnectionCredentials/NetworkHandlerAuthForm.tsx index 31c2e70e20..89693e5e79 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx +++ b/webapp/packages/core-connections/src/ConnectionCredentials/NetworkHandlerAuthForm.tsx @@ -11,39 +11,36 @@ import styled from 'reshadow'; import { BASE_CONTAINERS_STYLES, FieldCheckboxNew, GroupTitle, InputFieldNew, useMapResource } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; +import type { NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; import { NetworkHandlerResource } from '../NetworkHandlerResource'; -import type { IFormInitConfig } from './IFormInitConfig'; interface Props { - config: IFormInitConfig; - sshHandlerId: string; - allowPasswordSave: boolean; - disabled: boolean; - className?: string; + id: string; + networkHandlersConfig: NetworkHandlerConfigInput[]; + allowSaveCredentials?: boolean; + disabled?: boolean; } -export const SSHAuthForm: React.FC = observer(function SSHAuthForm({ - config, sshHandlerId, allowPasswordSave, disabled, className, -}) { +export const NetworkHandlerAuthForm: React.FC = observer(function NetworkHandlerAuthForm({ id, networkHandlersConfig, allowSaveCredentials, disabled }) { const translate = useTranslate(); - const handler = useMapResource(NetworkHandlerResource, sshHandlerId); + const handler = useMapResource(NetworkHandlerResource, id); - if (!config.networkCredentials.some(state => state.id === sshHandlerId)) { - config.networkCredentials.push({ - id: sshHandlerId, + if (!networkHandlersConfig.some(state => state.id === id)) { + networkHandlersConfig!.push({ + id: id, userName: '', password: '', savePassword: false, }); } - const state = config.networkCredentials.find(state => state.id === sshHandlerId)!; + const state = networkHandlersConfig.find(state => state.id === id)!; return styled(useStyles(BASE_CONTAINERS_STYLES))( <> - {translate(handler.data?.label || 'connections_network_handler_ssh_tunnel_title')} + {handler.data?.label || translate(`connections_network_handler_${id}_title`, 'connections_network_handler_default_title')} = observer(function SSHAuthForm({ disabled={disabled} mod='surface' > - {translate('connections_network_handler_ssh_tunnel_user')} + {translate(`connections_network_handler_${id}_user`, 'connections_network_handler_default_user')} = observer(function SSHAuthForm({ disabled={disabled} mod='surface' > - {translate('connections_network_handler_ssh_tunnel_password')} + {translate(`connections_network_handler_${id}_password`, 'connections_network_handler_default_password')} - {allowPasswordSave && ( + {allowSaveCredentials && ( = observer(function NetworkHandlers({ networkHandlers, networkHandlersConfig, allowSaveCredentials, disabled }) { + const styles = useStyles(BASE_CONTAINERS_STYLES); + + if (!networkHandlers.length) { + return null; + } + + return styled(styles)( + + {networkHandlers.map(handler => ( + + ))} + + ); +}); diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts index 38d9e0b6a5..bb73892c48 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts @@ -12,9 +12,9 @@ import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import type { MetadataValueGetter } from '@cloudbeaver/core-utils'; import { ConnectionsResource } from '../../Administration/ConnectionsResource'; -import { connectionConfigContext } from '../connectionConfigContext'; import { ConnectionFormService } from '../ConnectionFormService'; -import { connectionFormStateContext } from '../connectionFormStateContext'; +import { connectionConfigContext } from '../Contexts/connectionConfigContext'; +import { connectionFormStateContext } from '../Contexts/connectionFormStateContext'; import type { IConnectionFormProps, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps'; import { ConnectionAccess } from './ConnectionAccess'; import type { IConnectionAccessTabState } from './IConnectionAccessTabState'; diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionForm.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionForm.tsx index f0764a4b04..9445d1dbbb 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionForm.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionForm.tsx @@ -16,8 +16,8 @@ import { useTranslate } from '@cloudbeaver/core-localization'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; import { useStyles, composes } from '@cloudbeaver/core-theming'; -import { connectionConfigContext } from './connectionConfigContext'; import { ConnectionFormService } from './ConnectionFormService'; +import { connectionConfigContext } from './Contexts/connectionConfigContext'; import type { IConnectionFormState } from './IConnectionFormProps'; const tabsStyles = css` diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormService.ts b/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormService.ts index 322e62df11..f88bec7d08 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormService.ts @@ -8,10 +8,14 @@ import { PlaceholderContainer, TabsContainer } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; import { ExecutorHandlersCollection, ExecutorInterrupter, IExecutorHandler, IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; +import { ConnectionCredentialsDialog } from '../ConnectionCredentials/ConnectionCredentialsDialog'; import { ConnectionFormBaseActions } from './ConnectionFormBaseActions'; +import { connectionConfigContext } from './Contexts/connectionConfigContext'; +import { connectionCredentialsStateContext } from './Contexts/connectionCredentialsStateContext'; import type { IConnectionFormProps, IConnectionFormState, IConnectionFormFillConfigData, IConnectionFormSubmitData } from './IConnectionFormProps'; export interface IConnectionFormValidation { @@ -42,7 +46,8 @@ export class ConnectionFormService { readonly formStateTask: IExecutorHandlersCollection; constructor( - private readonly notificationService: NotificationService + private readonly notificationService: NotificationService, + private readonly commonDialogService: CommonDialogService, ) { this.tabsContainer = new TabsContainer(); this.actionsContainer = new PlaceholderContainer(); @@ -60,6 +65,7 @@ export class ConnectionFormService { this.formStateTask .before(this.prepareConfigTask, state => ({ state, submitType: 'submit' })); + this.prepareConfigTask.addPostHandler(this.ensureCredentialsExisting); this.formSubmittingTask.addPostHandler(this.showSubmittingStatusMessage); this.formValidationTask.addPostHandler(this.ensureValidation); @@ -115,6 +121,35 @@ export class ConnectionFormService { } }; + private ensureCredentialsExisting: IExecutorHandler = async (data, contexts) => { + const credentialsState = contexts.getContext(connectionCredentialsStateContext); + + if (data.submitType !== 'test' || (!credentialsState.authModelId && !credentialsState.networkHandlers.length)) { + return; + } + + const config = contexts.getContext(connectionConfigContext); + + if (config.credentials === undefined && credentialsState.authModelId) { + config.credentials = {}; + } + + if (config.networkHandlersConfig === undefined && credentialsState.networkHandlers.length > 0) { + config.networkHandlersConfig = []; + } + + const result = await this.commonDialogService.open(ConnectionCredentialsDialog, { + config, + authModelId: credentialsState.authModelId, + networkHandlers: credentialsState.networkHandlers, + driverId: config.driverId, + }); + + if (result === DialogueStateResult.Rejected) { + ExecutorInterrupter.interrupt(contexts); + } + }; + private ensureValidation: IExecutorHandler = (data, contexts) => { const validation = contexts.getContext(this.connectionValidationContext); diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormState.ts b/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormState.ts index 7d1f5c1b1a..d7156d57a5 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormState.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionFormState.ts @@ -16,7 +16,7 @@ import type { DatabaseConnection } from '../Administration/ConnectionsResource'; import { EConnectionFeature } from '../EConnectionFeature'; import { connectionFormConfigureContext } from './connectionFormConfigureContext'; import type { ConnectionFormService } from './ConnectionFormService'; -import { connectionFormStateContext, IConnectionFormStateInfo } from './connectionFormStateContext'; +import { connectionFormStateContext, IConnectionFormStateInfo } from './Contexts/connectionFormStateContext'; import type { IConnectionFormState, ConnectionFormMode, ConnectionFormType, IConnectionFormSubmitData } from './IConnectionFormProps'; export class ConnectionFormState implements IConnectionFormState { diff --git a/webapp/packages/core-connections/src/ConnectionForm/connectionConfigContext.ts b/webapp/packages/core-connections/src/ConnectionForm/Contexts/connectionConfigContext.ts similarity index 100% rename from webapp/packages/core-connections/src/ConnectionForm/connectionConfigContext.ts rename to webapp/packages/core-connections/src/ConnectionForm/Contexts/connectionConfigContext.ts diff --git a/webapp/packages/core-connections/src/ConnectionForm/Contexts/connectionCredentialsStateContext.ts b/webapp/packages/core-connections/src/ConnectionForm/Contexts/connectionCredentialsStateContext.ts new file mode 100644 index 0000000000..e824ecaae2 --- /dev/null +++ b/webapp/packages/core-connections/src/ConnectionForm/Contexts/connectionCredentialsStateContext.ts @@ -0,0 +1,30 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +interface IConnectionCredentialsState { + authModelId: string | null; + networkHandlers: string[]; +} + +interface IConnectionCredentialsStateContext extends IConnectionCredentialsState { + requireAuthModel: (id: string) => void; + requireNetworkHandler: (id: string) => void; +} + +export function connectionCredentialsStateContext(): IConnectionCredentialsStateContext { + return { + authModelId: null, + networkHandlers: [], + requireAuthModel(id) { + this.authModelId = id; + }, + requireNetworkHandler(id) { + this.networkHandlers.push(id); + }, + }; +} diff --git a/webapp/packages/core-connections/src/ConnectionForm/connectionFormStateContext.ts b/webapp/packages/core-connections/src/ConnectionForm/Contexts/connectionFormStateContext.ts similarity index 100% rename from webapp/packages/core-connections/src/ConnectionForm/connectionFormStateContext.ts rename to webapp/packages/core-connections/src/ConnectionForm/Contexts/connectionFormStateContext.ts diff --git a/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts b/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts index 77c63c7a8d..6dc3647e08 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts @@ -11,9 +11,9 @@ import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { isObjectPropertyInfoStateEqual } from '@cloudbeaver/core-sdk'; import { DBDriverResource } from '../../DBDriverResource'; -import { connectionConfigContext } from '../connectionConfigContext'; import { ConnectionFormService } from '../ConnectionFormService'; -import { connectionFormStateContext } from '../connectionFormStateContext'; +import { connectionConfigContext } from '../Contexts/connectionConfigContext'; +import { connectionFormStateContext } from '../Contexts/connectionFormStateContext'; import type { IConnectionFormFillConfigData, IConnectionFormSubmitData, IConnectionFormState } from '../IConnectionFormProps'; import { DriverProperties } from './DriverProperties'; diff --git a/webapp/packages/core-connections/src/ConnectionForm/IConnectionFormProps.ts b/webapp/packages/core-connections/src/ConnectionForm/IConnectionFormProps.ts index 1744fb8af6..9142380b81 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/IConnectionFormProps.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/IConnectionFormProps.ts @@ -11,7 +11,7 @@ import type { CachedMapResource, ConnectionConfig, GetConnectionsQueryVariables import type { MetadataMap } from '@cloudbeaver/core-utils'; import type { DatabaseConnection } from '../Administration/ConnectionsResource'; -import type { IConnectionFormStateInfo } from './connectionFormStateContext'; +import type { IConnectionFormStateInfo } from './Contexts/connectionFormStateContext'; export type ConnectionFormMode = 'edit' | 'create'; export type ConnectionFormType = 'admin' | 'public'; diff --git a/webapp/packages/core-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts b/webapp/packages/core-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts index a8bc178477..bf06b5cea5 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts @@ -17,13 +17,16 @@ import { DatabaseAuthModelsResource } from '../../DatabaseAuthModelsResource'; import { DBDriverResource } from '../../DBDriverResource'; import { getUniqueConnectionName } from '../../getUniqueConnectionName'; import { isJDBCConnection } from '../../isJDBCConnection'; -import { connectionConfigContext } from '../connectionConfigContext'; import { connectionFormConfigureContext } from '../connectionFormConfigureContext'; import { ConnectionFormService } from '../ConnectionFormService'; -import { connectionFormStateContext } from '../connectionFormStateContext'; +import { connectionConfigContext } from '../Contexts/connectionConfigContext'; +import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext'; +import { connectionFormStateContext } from '../Contexts/connectionFormStateContext'; import type { IConnectionFormSubmitData, IConnectionFormFillConfigData, IConnectionFormState } from '../IConnectionFormProps'; import { Options } from './Options'; +const USER_NAME_AUTH_PROPERTY_ID = 'userName'; + @injectable() export class ConnectionOptionsTabService extends Bootstrap { constructor( @@ -124,7 +127,7 @@ export class ConnectionOptionsTabService extends Bootstrap { } } - private validate( + private async validate( { state, }: IConnectionFormSubmitData, @@ -135,6 +138,15 @@ export class ConnectionOptionsTabService extends Bootstrap { if (!state.config.name?.length) { validation.error("Field 'name' can't be empty"); } + + if (state.config.authModelId && state.config.saveCredentials) { + const authProperties = await this.getConnectionAuthModelProperties(state.config.authModelId, state.info); + const userNameProperty = authProperties.find(property => property.id === USER_NAME_AUTH_PROPERTY_ID); + + if (userNameProperty && (!state.config.credentials || !state.config.credentials[USER_NAME_AUTH_PROPERTY_ID])) { + validation.error(`Field '${userNameProperty.displayName || 'User name'}' can't be empty`); + } + } } private fillConfig( @@ -199,6 +211,7 @@ export class ConnectionOptionsTabService extends Bootstrap { contexts: IExecutionContextProvider ) { const config = contexts.getContext(connectionConfigContext); + const credentialsState = contexts.getContext(connectionCredentialsStateContext); const driver = await this.dbDriverResource.load(state.config.driverId!, ['includeProviderProperties']); @@ -240,7 +253,15 @@ export class ConnectionOptionsTabService extends Bootstrap { const properties = await this.getConnectionAuthModelProperties(config.authModelId, state.info); if (this.isCredentialsChanged(properties, state.config.credentials)) { - config.credentials = state.config.credentials; + config.credentials = { ...state.config.credentials }; + } + + if ( + config.authModelId + && !state.config?.saveCredentials + && (!state.config.credentials || hasMissingCredentials(properties, state.config.credentials)) + ) { + credentialsState.requireAuthModel(config.authModelId); } } @@ -332,3 +353,8 @@ export class ConnectionOptionsTabService extends Bootstrap { return properties; } } + +function hasMissingCredentials(authProperties: ObjectPropertyInfo[], credentials: Record) { + return authProperties.some(property => property.id === USER_NAME_AUTH_PROPERTY_ID) + && !credentials[USER_NAME_AUTH_PROPERTY_ID]; +} diff --git a/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts b/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts index ba344b4732..8ced417ce0 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts @@ -13,7 +13,7 @@ import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { isLocalConnection } from '../../Administration/ConnectionsResource'; import { connectionFormConfigureContext } from '../connectionFormConfigureContext'; import { ConnectionFormService } from '../ConnectionFormService'; -import { connectionFormStateContext } from '../connectionFormStateContext'; +import { connectionFormStateContext } from '../Contexts/connectionFormStateContext'; import type { IConnectionFormState } from '../IConnectionFormProps'; import { ConnectionFormAuthenticationAction } from './ConnectionFormAuthenticationAction'; import { OriginInfo } from './OriginInfo'; diff --git a/webapp/packages/core-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts b/webapp/packages/core-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts index 0de6be06a1..b6bfd1c2a5 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts @@ -13,10 +13,11 @@ import type { NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; import type { DatabaseConnection } from '../../Administration/ConnectionsResource'; import { DBDriverResource } from '../../DBDriverResource'; import { SSH_TUNNEL_ID } from '../../NetworkHandlerResource'; -import { connectionConfigContext } from '../connectionConfigContext'; import { connectionFormConfigureContext } from '../connectionFormConfigureContext'; import { ConnectionFormService } from '../ConnectionFormService'; -import { connectionFormStateContext } from '../connectionFormStateContext'; +import { connectionConfigContext } from '../Contexts/connectionConfigContext'; +import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext'; +import { connectionFormStateContext } from '../Contexts/connectionFormStateContext'; import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps'; import { SSH } from './SSH'; import { SSHTab } from './SSHTab'; @@ -118,13 +119,16 @@ export class ConnectionSSHTabService extends Bootstrap { } for (const handler of config.networkHandlersConfig) { - if (handler.enabled && handler.savePassword && this.isChanged(handler, info)) { - if (!handler.userName?.length) { - validation.error("Field SSH 'User' can't be empty"); - } - if (!handler.password?.length) { - validation.error("Field SSH 'Password' can't be empty"); + if (handler.enabled && this.isChanged(handler, info)) { + if (handler.savePassword) { + if (!handler.userName?.length) { + validation.error("Field SSH 'User' can't be empty"); + } + if (!handler.password?.length) { + validation.error("Field SSH 'Password' can't be empty"); + } } + if (!handler.properties?.host?.length) { validation.error("Field SSH 'Host' can't be empty"); } @@ -144,22 +148,31 @@ export class ConnectionSSHTabService extends Bootstrap { contexts: IExecutionContextProvider ) { const config = contexts.getContext(connectionConfigContext); + const credentialsState = contexts.getContext(connectionCredentialsStateContext); if (!state.config.networkHandlersConfig || state.config.networkHandlersConfig.length === 0) { return; } const configs: NetworkHandlerConfigInput[] = []; + let sshHandler: NetworkHandlerConfigInput | undefined; for (const handler of state.config.networkHandlersConfig) { if (this.isChanged(handler, state.info)) { - configs.push(handler); + configs.push({ ...handler }); + } + if (handler.id === SSH_TUNNEL_ID) { + sshHandler = handler; } } if (configs.length > 0) { config.networkHandlersConfig = configs; } + + if (sshHandler?.enabled && !sshHandler.savePassword && hasMissingCredentials(sshHandler)) { + credentialsState.requireNetworkHandler(SSH_TUNNEL_ID); + } } private formState( @@ -198,3 +211,7 @@ export class ConnectionSSHTabService extends Bootstrap { return false; } } + +function hasMissingCredentials(handler: NetworkHandlerConfigInput): boolean { + return !handler.userName; +} diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/DBAuthDialogController.ts b/webapp/packages/core-connections/src/DatabaseAuthDialog/DBAuthDialogController.ts index a8314238f9..e0140dc8fc 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/DBAuthDialogController.ts +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/DBAuthDialogController.ts @@ -14,16 +14,16 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; import { GQLErrorCatcher } from '@cloudbeaver/core-sdk'; +import type { IConnectionAuthCredentialsConfig } from '../ConnectionCredentials/IConnectionAuthCredentialsConfig'; import { ConnectionInfoResource, ConnectionInitConfig } from '../ConnectionInfoResource'; import { DBDriverResource } from '../DBDriverResource'; -import type { IFormInitConfig } from './IFormInitConfig'; @injectable() export class DBAuthDialogController implements IInitializableController, IDestructibleController { isAuthenticating = false; - config: IFormInitConfig = { + config: IConnectionAuthCredentialsConfig = { credentials: {}, - networkCredentials: [], + networkHandlersConfig: [], saveCredentials: false, }; @@ -45,7 +45,7 @@ export class DBAuthDialogController implements IInitializableController, IDestru }); } - init(connectionId: string, onClose: () => void) { + init(connectionId: string, onClose: () => void): void { this.connectionId = connectionId; this.close = onClose; this.loadAuthModel(); @@ -56,7 +56,7 @@ export class DBAuthDialogController implements IInitializableController, IDestru this.isDistructed = true; } - login = async () => { + login = async (): Promise => { if (this.isAuthenticating) { return; } @@ -74,7 +74,7 @@ export class DBAuthDialogController implements IInitializableController, IDestru } }; - showDetails = () => { + showDetails = (): void => { if (this.error.exception) { this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); } @@ -90,8 +90,8 @@ export class DBAuthDialogController implements IInitializableController, IDestru config.saveCredentials = this.config.saveCredentials; } - if (this.config.networkCredentials.length > 0) { - config.networkCredentials = this.config.networkCredentials; + if (this.config.networkHandlersConfig.length > 0) { + config.networkCredentials = this.config.networkHandlersConfig; } return config; diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx index f8ba8139f3..6f47210c08 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx @@ -14,26 +14,18 @@ import { SubmittingForm, Loader, useFocus, - BASE_CONTAINERS_STYLES, - Container, - Group, - FieldCheckboxNew, - ObjectPropertyInfoFormNew, - GroupTitle, ErrorMessage, - TextPlaceholder, } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; -import { SSH_TUNNEL_ID } from '../NetworkHandlerResource'; +import { ConnectionCredentialsForm } from '../ConnectionCredentials/ConnectionCredentialsForm'; import { useConnectionInfo } from '../useConnectionInfo'; import { useDBDriver } from '../useDBDriver'; import { DBAuthDialogController } from './DBAuthDialogController'; import { DBAuthDialogFooter } from './DBAuthDialogFooter'; -import { SSHAuthForm } from './SSHAuthForm'; const styles = composes( css` @@ -53,7 +45,7 @@ const styles = composes( display: flex; flex-direction: column; } - Container { + ConnectionCredentialsForm { align-content: center; } ErrorMessage { @@ -61,31 +53,33 @@ const styles = composes( } `); +interface Payload { + connectionId: string; + networkHandlers: string[]; +} + export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({ payload, options, rejectDialog, -}: DialogComponentProps) { - const connection = useConnectionInfo(payload); - - const [focusedRef] = useFocus({ focusFirstChild: true }); - const { driver } = useDBDriver(connection.connectionInfo?.driverId || ''); - const controller = useController(DBAuthDialogController, payload, rejectDialog); +}: DialogComponentProps) { const translate = useTranslate(); + const connection = useConnectionInfo(payload.connectionId); + const controller = useController(DBAuthDialogController, payload.connectionId, rejectDialog); + + const { driver } = useDBDriver(connection.connectionInfo?.driverId || ''); const { credentialsSavingEnabled } = useAdministrationSettings(); + const [focusedRef] = useFocus({ focusFirstChild: true }); - const sshConfig = connection.connectionInfo?.networkHandlersConfig.find( - handler => handler.id === SSH_TUNNEL_ID - ); + let authModelId: string | null = null; - const isAuthNeeded = connection.connectionInfo?.authNeeded; - const isSSHAuthNeeded = sshConfig?.enabled && !sshConfig.savePassword; + if (connection.connectionInfo?.authNeeded) { + authModelId = connection.connectionInfo?.authModel || driver?.defaultAuthModel || null; + } - const title = translate('connections_database_authentication'); - - return styled(useStyles(styles, BASE_CONTAINERS_STYLES))( + return styled(useStyles(styles))( : ( - - {isAuthNeeded && ( - - {connection.connectionInfo?.authProperties ? ( - <> - {isSSHAuthNeeded && sshConfig && {title}} - - {credentialsSavingEnabled && ( - - )} - - ) : ( - Authentication data is not avaliable - )} - - )} - {isSSHAuthNeeded && ( - - {sshConfig ? ( - - ) : ( - SSH Authentication data is not avaliable - )} - - )} - + )} diff --git a/webapp/packages/core-connections/src/index.ts b/webapp/packages/core-connections/src/index.ts index d6c3513473..c276ec0310 100644 --- a/webapp/packages/core-connections/src/index.ts +++ b/webapp/packages/core-connections/src/index.ts @@ -16,15 +16,17 @@ export * from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabSe export * from './ConnectionForm/SSH/ConnectionSSHTabService'; export * from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService'; export * from './ConnectionForm/ConnectionAccess/ConnectionAccessTabService'; -export * from './ConnectionForm/connectionConfigContext'; +export * from './ConnectionForm/Contexts/connectionConfigContext'; export * from './ConnectionForm/ConnectionFormBaseActions'; export * from './ConnectionForm/connectionFormConfigureContext'; export * from './ConnectionForm/ConnectionForm'; export * from './ConnectionForm/ConnectionFormService'; export * from './ConnectionForm/ConnectionFormState'; -export * from './ConnectionForm/connectionFormStateContext'; +export * from './ConnectionForm/Contexts/connectionFormStateContext'; export * from './ConnectionForm/IConnectionFormProps'; export * from './ConnectionForm/useConnectionFormState'; +export * from './ConnectionCredentials/IConnectionAuthCredentialsConfig'; +export * from './ConnectionCredentials/ConnectionCredentialsForm'; export * from './extensions/IConnectionProvider'; export * from './extensions/IConnectionSetter'; @@ -35,8 +37,6 @@ export * from './EConnectionFeature'; export * from './ContainerResource'; export * from './ConnectionsLocaleService'; export * from './DatabaseAuthModelsResource'; -export * from './DatabaseAuthDialog/IFormInitConfig'; -export * from './DatabaseAuthDialog/SSHAuthForm'; export * from './DBDriverResource'; export * from './NetworkHandlerResource'; export * from './useConnectionInfo'; diff --git a/webapp/packages/core-connections/src/locales/en.ts b/webapp/packages/core-connections/src/locales/en.ts index e9736eb6c2..e97ac087b6 100644 --- a/webapp/packages/core-connections/src/locales/en.ts +++ b/webapp/packages/core-connections/src/locales/en.ts @@ -22,6 +22,8 @@ export default [ ['connections_connection_create_custom', 'Custom'], ['connections_connection_create_search_database', 'Search'], ['connections_connection_edit_save_credentials', 'Save credentials'], + ['connections_connection_credentials_provisioning', 'Credentials provisioning'], + ['connections_connection_credentials_provisioning_description', 'You need to provide additional credentials to test this connection'], ['connections_connection_edit_authentication', 'Authentication'], ['connections_connection_edit_settings', 'Settings'], ['connections_connection_edit_access', 'Access'], @@ -55,6 +57,9 @@ export default [ ['connections_network_handler_test', 'Test Tunnel'], ['connections_network_handler_test_fail', 'Tunnel test failed'], ['connections_network_handler_test_success', 'Tunnel test success'], + ['connections_network_handler_default_title', 'Network handler'], + ['connections_network_handler_default_user', 'User'], + ['connections_network_handler_default_password', 'Password'], ['connections_network_handler_ssh_tunnel_title', 'SSH Tunnel'], ['connections_network_handler_ssh_tunnel_enable', 'Use SSH Tunnel'], ['connections_network_handler_ssh_tunnel_host', 'Host'], diff --git a/webapp/packages/core-connections/src/locales/ru.ts b/webapp/packages/core-connections/src/locales/ru.ts index 5d05b3e05c..f6b5b890ef 100644 --- a/webapp/packages/core-connections/src/locales/ru.ts +++ b/webapp/packages/core-connections/src/locales/ru.ts @@ -19,6 +19,8 @@ export default [ ['connections_connection_create_custom', 'Настроить'], ['connections_connection_create_search_database', 'Найти'], ['connections_connection_edit_save_credentials', ' Запомнить данные'], + ['connections_connection_credentials_provisioning', 'Предоставление данных доступа'], + ['connections_connection_credentials_provisioning_description', 'Чтобы проверить подключение, необходимо предоставить дополнительные данные доступа'], ['connections_connection_edit_authentication', 'Авторизация'], ['connections_connection_edit_settings', 'Настройки'], ['connections_connection_edit_access', 'Доступ'], @@ -52,6 +54,9 @@ export default [ ['connections_network_handler_test', 'Проверить подключение'], ['connections_network_handler_test_fail', 'Не удалось установить соединение'], ['connections_network_handler_test_success', 'Соединение установлено'], + ['connections_network_handler_default_title', 'Сетевой обработчик'], + ['connections_network_handler_default_user', 'Пользователь'], + ['connections_network_handler_default_password', 'Пароль'], ['connections_network_handler_ssh_tunnel_title', 'SSH Тунель'], ['connections_network_handler_ssh_tunnel_enable', 'Использовать SSH Тунель'], ['connections_network_handler_ssh_tunnel_host', 'Хост'], diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index dba14dd0aa..27e4c6cb4d 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -13,6 +13,7 @@ export const defaultENLocale = [ ['ui_processing_ok', 'Ok'], ['ui_processing_create', 'Create'], ['ui_processing_save', 'Save'], + ['ui_apply', 'Apply'], ['ui_error_message', 'Error:'], ['ui_error_close', 'Close'], ['ui_close', 'Close'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 7af94f30d4..c8efad5d9c 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -11,6 +11,7 @@ export const defaultRULocale = [ ['ui_processing_ok', 'Принять'], ['ui_processing_create', 'Создать'], ['ui_processing_save', 'Сохранить'], + ['ui_apply', 'Применить'], ['ui_error_message', 'Ошибка:'], ['ui_error_close', 'Закрыть'], ['ui_close', 'Закрыть'], diff --git a/webapp/packages/core-utils/src/getCookies.ts b/webapp/packages/core-utils/src/getCookies.ts index 8668458670..741453a284 100644 --- a/webapp/packages/core-utils/src/getCookies.ts +++ b/webapp/packages/core-utils/src/getCookies.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -type Cookies = Record; +type Cookies = Partial>; export function getCookies(): Cookies { if (!document.cookie) { diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts index 95fd2f14a1..83bc5f3f2b 100644 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts +++ b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts @@ -9,7 +9,7 @@ import { observable, makeObservable } from 'mobx'; import { DBDriverResource, Connection, DatabaseAuthModelsResource, ConnectionInfoResource, DBDriver, ConnectionInitConfig, getUniqueConnectionName } from '@cloudbeaver/core-connections'; -import type { IFormInitConfig } from '@cloudbeaver/core-connections'; +import type { IConnectionAuthCredentialsConfig } from '@cloudbeaver/core-connections'; import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -25,7 +25,7 @@ export enum ConnectionStep { export interface IConnectionController { template: Connection | null; - config: IFormInitConfig; + config: IConnectionAuthCredentialsConfig; isConnecting: boolean; onConnect: () => void; } @@ -38,9 +38,9 @@ implements IInitializableController, IDestructibleController, IConnectionControl isConnecting = false; template: Connection | null = null; authModel?: DatabaseAuthModel; - config: IFormInitConfig = { + config: IConnectionAuthCredentialsConfig = { credentials: {}, - networkCredentials: [], + networkHandlersConfig: [], saveCredentials: false, }; @@ -95,7 +95,7 @@ implements IInitializableController, IDestructibleController, IConnectionControl this.isDistructed = true; } - onStep = (step: ConnectionStep) => { + onStep = (step: ConnectionStep): void => { this.step = step; this.clearError(); @@ -104,7 +104,7 @@ implements IInitializableController, IDestructibleController, IConnectionControl } }; - onConnect = async () => { + onConnect = async (): Promise => { if (!this.template) { return; } @@ -132,14 +132,14 @@ implements IInitializableController, IDestructibleController, IConnectionControl } }; - onTemplateSelect = async (templateId: string) => { + onTemplateSelect = async (templateId: string): Promise => { this.template = this.templateConnections.find(template => template.id === templateId)!; await this.loadAuthModel(); this.clearError(); this.config = { credentials: {}, - networkCredentials: [], + networkHandlersConfig: [], saveCredentials: false, }; @@ -149,7 +149,7 @@ implements IInitializableController, IDestructibleController, IConnectionControl } }; - onShowDetails = () => { + onShowDetails = (): void => { if (this.exception) { this.commonDialogService.open(ErrorDetailsDialog, this.exception); } @@ -165,8 +165,8 @@ implements IInitializableController, IDestructibleController, IConnectionControl config.saveCredentials = this.config.saveCredentials; } - if (this.config.networkCredentials.length > 0) { - config.networkCredentials = this.config.networkCredentials; + if (this.config.networkHandlersConfig.length > 0) { + config.networkCredentials = this.config.networkHandlersConfig; } return config; diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx index 6e42678578..cae8f918c3 100644 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx +++ b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx @@ -15,14 +15,8 @@ import { SubmittingForm, Loader, useFocus, - Container, - Group, - FieldCheckboxNew, - BASE_CONTAINERS_STYLES, - ObjectPropertyInfoFormNew, - GroupTitle } from '@cloudbeaver/core-blocks'; -import { SSH_TUNNEL_ID, SSHAuthForm } from '@cloudbeaver/core-connections'; +import { SSH_TUNNEL_ID, ConnectionCredentialsForm } from '@cloudbeaver/core-connections'; import { useController } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs'; import { useTranslate } from '@cloudbeaver/core-localization'; @@ -73,10 +67,15 @@ export const ConnectionDialog = observer(function ConnectionDialog({ const sshConfig = controller.template?.networkHandlersConfig.find( handler => handler.id === SSH_TUNNEL_ID ); + const sshAuthRequired = sshConfig?.enabled && !sshConfig.savePassword; - const isSSHAuthNeeded = sshConfig?.enabled && !sshConfig.savePassword; + const networkHandlers: string[] = []; - return styled(useStyles(styles, BASE_CONTAINERS_STYLES))( + if (sshAuthRequired) { + networkHandlers.push(SSH_TUNNEL_ID); + } + + return styled(useStyles(styles))( ) : ( - - - {isSSHAuthNeeded && {translate('connections_database_authentication')}} - - {credentialsSavingEnabled && ( - - )} - - {isSSHAuthNeeded && sshConfig && ( - - )} - + ))} {controller.responseMessage && (