From 457ee9533199a6235893fe8a29c204c186fff165 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Thu, 27 Feb 2025 17:36:03 +0100 Subject: [PATCH] CB-4067 uses new form parts for connection form --- .../core-ui/src/Form/FormBaseService.ts | 2 + webapp/packages/core-ui/src/Form/FormPart.ts | 2 + webapp/packages/core-ui/src/Form/FormState.ts | 4 + .../packages/core-ui/src/Form/IFormState.ts | 1 + .../src/Search/ConnectionSearchService.ts | 67 ++- .../ConnectionsTable/ConnectionEdit.tsx | 33 +- .../CreateConnection/CreateConnection.tsx | 2 +- .../Connections/CreateConnectionService.ts | 41 +- .../ConnectionAccess/ConnectionAccess.tsx | 54 ++- .../ConnectionAccessTabService.ts | 136 +----- .../ConnectionFormAccessPart.ts | 25 +- .../IConnectionFormAccessState.ts | 1 + .../useConnectionAccessState.ts | 106 ----- .../src/ConnectionForm/ConnectionForm.tsx | 59 +-- .../ConnectionForm/ConnectionFormService.ts | 182 ------- .../src/ConnectionForm/ConnectionFormState.ts | 342 -------------- .../ConnectionFormStateRefactored.ts | 2 +- .../ConnectionDriverPropertiesTabService.ts | 119 +---- .../ConnectionFormDriverPropertiesPart.ts | 18 +- .../DriverProperties/DriverProperties.tsx | 20 +- .../getConnectionFormDriverPropertiesPart.ts | 2 +- .../ConnectionForm/IConnectionFormProps.ts | 5 - .../IConnectionFormStateRefactored.ts | 8 +- .../Options/ConnectionFormOpitonsPart.ts | 119 +++-- .../Options/ConnectionOptionsTabService.ts | 446 +----------------- .../Options/IConnectionConfig.ts | 43 ++ .../Options/IConnectionFormOptionsState.ts | 46 +- .../Options/IConnectionNetworkHanler.ts | 23 + .../src/ConnectionForm/Options/Options.tsx | 80 ++-- .../src/ConnectionForm/Options/useOptions.ts | 34 +- .../ConnectionFormAuthenticationAction.tsx | 55 ++- .../ConnectionOriginInfoTabService.ts | 71 +-- .../ConnectionForm/OriginInfo/OriginInfo.tsx | 18 +- .../OriginInfo/OriginInfoTab.tsx | 15 +- .../SSH/ConnectionFormSSHPart.ts | 38 +- .../SSH/ConnectionSSHTabService.ts | 262 +--------- .../SSH/IConnectionFromSSHState.ts | 2 +- .../src/ConnectionForm/SSH/SSH.tsx | 59 ++- .../src/ConnectionForm/SSH/SSHPanel.tsx | 11 +- .../src/ConnectionForm/SSH/SSHTab.tsx | 4 +- .../SSL/ConnectionFormSSLPart.ts | 38 +- .../SSL/ConnectionSSLTabService.ts | 216 +-------- .../src/ConnectionForm/SSL/SSL.tsx | 37 +- .../src/ConnectionForm/SSL/SSLPanel.tsx | 28 +- .../src/ConnectionForm/SSL/SSLTab.tsx | 6 +- .../connectionFormConfigureContext.ts | 16 +- .../ConnectionForm/useConnectionFormState.ts | 38 -- .../useConnectionFormStateRefactored.ts | 31 +- .../PublicConnectionFormService.ts | 77 ++- .../packages/plugin-connections/src/index.ts | 5 +- .../plugin-connections/src/manifest.ts | 1 - 51 files changed, 690 insertions(+), 2360 deletions(-) delete mode 100644 webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts delete mode 100644 webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormService.ts delete mode 100644 webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts create mode 100644 webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionConfig.ts create mode 100644 webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionNetworkHanler.ts delete mode 100644 webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormState.ts diff --git a/webapp/packages/core-ui/src/Form/FormBaseService.ts b/webapp/packages/core-ui/src/Form/FormBaseService.ts index 7e699d3077..0f6f61024b 100644 --- a/webapp/packages/core-ui/src/Form/FormBaseService.ts +++ b/webapp/packages/core-ui/src/Form/FormBaseService.ts @@ -23,6 +23,7 @@ export class FormBaseService = IFormPr readonly onFormat: IExecutorHandlersCollection>; readonly onValidate: IExecutorHandlersCollection>; readonly onSubmit: IExecutorHandlersCollection>; + readonly onConfigure: IExecutorHandlersCollection>; readonly onLoaded: IExecutorHandlersCollection>; readonly onState: IExecutorHandlersCollection; @@ -36,6 +37,7 @@ export class FormBaseService = IFormPr this.onFormat = new ExecutorHandlersCollection(); this.onValidate = new ExecutorHandlersCollection(); this.onSubmit = new ExecutorHandlersCollection(); + this.onConfigure = new ExecutorHandlersCollection(); this.onLoaded = new ExecutorHandlersCollection(); this.onState = new ExecutorHandlersCollection(); diff --git a/webapp/packages/core-ui/src/Form/FormPart.ts b/webapp/packages/core-ui/src/Form/FormPart.ts index 995a22626d..a91f009c75 100644 --- a/webapp/packages/core-ui/src/Form/FormPart.ts +++ b/webapp/packages/core-ui/src/Form/FormPart.ts @@ -38,6 +38,7 @@ export abstract class FormPart implements IFormPar this.loaded = false; this.loading = false; + this.formState.configureTask.addHandler(this.configure.bind(this)); this.formState.submitTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.save.bind(this))); this.formState.formatTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.format.bind(this))); this.formState.validationTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.validate.bind(this))); @@ -162,6 +163,7 @@ export abstract class FormPart implements IFormPar protected format(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} protected validate(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} + protected configure(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} protected abstract loader(): Promise; protected abstract saveChanges(data: IFormState, contexts: IExecutionContextProvider>): Promise; diff --git a/webapp/packages/core-ui/src/Form/FormState.ts b/webapp/packages/core-ui/src/Form/FormState.ts index 25bcc384d3..c39cc31d73 100644 --- a/webapp/packages/core-ui/src/Form/FormState.ts +++ b/webapp/packages/core-ui/src/Form/FormState.ts @@ -44,6 +44,7 @@ export class FormState implements IFormState { readonly dataContext: IDataContext; readonly formStateTask: IExecutor; + readonly configureTask: IExecutor>; readonly loadedTask: IExecutor>; readonly submitTask: IExecutor>; readonly formatTask: IExecutor>; @@ -69,6 +70,9 @@ export class FormState implements IFormState { this.loadedTask = new Executor(this as IFormState, () => true); this.loadedTask.addCollection(service.onLoaded).next(this.formStateTask).addPostHandler(this.onLoadedHandler.bind(this)); + this.configureTask = new Executor(this as IFormState, () => true); + this.configureTask.addCollection(service.onConfigure).before(this.loadedTask); + this.formatTask = new Executor(this as IFormState, () => true); this.formatTask.addCollection(service.onFormat); diff --git a/webapp/packages/core-ui/src/Form/IFormState.ts b/webapp/packages/core-ui/src/Form/IFormState.ts index 4b30e0589b..62d7f328a7 100644 --- a/webapp/packages/core-ui/src/Form/IFormState.ts +++ b/webapp/packages/core-ui/src/Form/IFormState.ts @@ -30,6 +30,7 @@ export interface IFormState { readonly statusMessage: string | string[] | null; readonly statusType: ENotificationType | null; + readonly configureTask: IExecutor>; readonly loadedTask: IExecutor>; readonly formStateTask: IExecutor; readonly submitTask: IExecutor>; diff --git a/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts b/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts index ab3ce752a7..e8a4a7bcac 100644 --- a/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts +++ b/webapp/packages/plugin-connection-search/src/Search/ConnectionSearchService.ts @@ -8,20 +8,14 @@ import { makeObservable, observable } from 'mobx'; import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; -import { - ConnectionInfoOriginResource, - ConnectionInfoResource, - ConnectionsManagerService, - createConnectionParam, -} from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { ConnectionInfoResource, ConnectionsManagerService, createConnectionParam } from '@cloudbeaver/core-connections'; +import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { ExecutorInterrupter, type IExecutorHandler } from '@cloudbeaver/core-executor'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; import type { AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; import { OptionsPanelService } from '@cloudbeaver/core-ui'; -import { ConnectionFormService, ConnectionFormState, type IConnectionFormState } from '@cloudbeaver/plugin-connections'; +import { ConnectionFormServiceRefactored, ConnectionFormStateRefactored } from '@cloudbeaver/plugin-connections'; import { SearchDatabase } from './SearchDatabase.js'; @@ -34,18 +28,16 @@ export class ConnectionSearchService { disabled = false; - formState: IConnectionFormState | null = null; + formState: ConnectionFormStateRefactored | null = null; constructor( private readonly notificationService: NotificationService, private readonly connectionInfoResource: ConnectionInfoResource, - private readonly connectionFormService: ConnectionFormService, + private readonly serviceProvider: IServiceProvider, private readonly optionsPanelService: OptionsPanelService, + private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, private readonly commonDialogService: CommonDialogService, - private readonly projectsService: ProjectsService, - private readonly projectInfoResource: ProjectInfoResource, private readonly connectionsManagerService: ConnectionsManagerService, - private readonly connectionInfoOriginResource: ConnectionInfoOriginResource, ) { this.optionsPanelService.closeTask.addHandler(this.closeHandler); @@ -114,16 +106,14 @@ export class ConnectionSearchService { if ( !this.formState || !this.optionsPanelService.isOpen(formGetter) || - (this.formState.config.connectionId && - this.formState.projectId !== null && - !this.connectionInfoResource.has(createConnectionParam(this.formState.projectId, this.formState.config.connectionId))) + (this.formState.state.config.connectionId && + this.formState.state.projectId !== null && + !this.connectionInfoResource.has(createConnectionParam(this.formState.state.projectId, this.formState.state.config.connectionId))) ) { return true; } - const state = await this.formState.checkFormState(); - - if (!state?.edited) { + if (!this.formState.isChanged) { return true; } @@ -157,32 +147,29 @@ export class ConnectionSearchService { } if (!this.formState) { - this.formState = new ConnectionFormState( - this.projectsService, - this.projectInfoResource, - this.connectionFormService, - this.connectionInfoResource, - this.connectionInfoOriginResource, - ); + this.formState = new ConnectionFormStateRefactored(this.serviceProvider, this.connectionFormServiceRefactored, { + config: { + ...this.connectionInfoResource.getEmptyConfig(), + host: database.host, + port: String(database.port), + driverId: database.defaultDriver, + }, + // TODO remove it? + submitType: null, + projectId: projects[0]!.id, + availableDrivers: database.possibleDrivers, + type: 'public', + }); - this.formState.closeTask.addHandler(this.goBack.bind(this)); + // this.formState.closeTask.addHandler(this.goBack.bind(this)); } - this.formState - .setOptions('create', 'public') - .setConfig(projects[0]!.id, { - ...this.connectionInfoResource.getEmptyConfig(), - driverId: database.defaultDriver, - host: database.host, - port: `${database.port}`, - }) - .setAvailableDrivers(database.possibleDrivers); - - this.formState.load(); + // this.formState.load(); } private clearFormState() { - this.formState?.dispose(); + // TODO dispose form state once we have this API + // this.formState?.dispose(); this.formState = null; } } diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx index 71c9ed2a0d..c79130a466 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx @@ -9,40 +9,43 @@ import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; import { Loader, s, useS } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoOriginResource, ConnectionInfoResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { ConnectionFormLoader, useConnectionFormState } from '@cloudbeaver/plugin-connections'; +import { type IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { ConnectionFormLoader, useConnectionFormStateRefactored } from '@cloudbeaver/plugin-connections'; import styles from './ConnectionEdit.module.css'; +import { FormMode } from '@cloudbeaver/core-ui'; interface Props { item: IConnectionInfoParams; } export const ConnectionEdit = observer(function ConnectionEditNew({ item }) { - const connectionInfoResource = useService(ConnectionInfoResource); - const connectionInfoOriginResource = useService(ConnectionInfoOriginResource); - // const tableContext = useContext(TableContext); - // const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext, item]); - - const data = useConnectionFormState(connectionInfoResource, connectionInfoOriginResource, state => state.setOptions('edit', 'admin')); + const data = useConnectionFormStateRefactored(item, state => { + state.setMode(FormMode.Edit); + state.setState({ + ...state.state, + config: { + ...state.state.config, + connectionId: item.connectionId, + }, + projectId: item.projectId, + type: 'admin', + }); + }); const style = useS(styles); const projectId = item.projectId; const connectionId = item.connectionId; useMemo(() => { - data.setConfig(projectId, { connectionId }); + data.state.config.connectionId = connectionId; + data.state.projectId = projectId; }, [data, projectId, connectionId]); return (
- +
); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx index 0eb9a0cea9..243773ce78 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx @@ -48,7 +48,7 @@ export const CreateConnection = observer(function CreateConnection({ meth const style = useS(styles); const createConnectionService = useService(CreateConnectionService); const translate = useTranslate(); - const driver = useResource(CreateConnection, DBDriverResource, createConnectionService.data?.config.driverId || null); + const driver = useResource(CreateConnection, DBDriverResource, createConnectionService.data?.state.config.driverId || null); if (createConnectionService.data) { return ( diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnectionService.ts b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnectionService.ts index d2f80db241..89e82aa0d0 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnectionService.ts +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnectionService.ts @@ -8,12 +8,10 @@ import { action, makeObservable, observable } from 'mobx'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { ConnectionInfoOriginResource, ConnectionInfoResource } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; import { TabsContainer } from '@cloudbeaver/core-ui'; -import { ConnectionFormService, ConnectionFormState, type IConnectionFormState } from '@cloudbeaver/plugin-connections'; +import { ConnectionFormServiceRefactored, ConnectionFormStateRefactored } from '@cloudbeaver/plugin-connections'; import { ConnectionsAdministrationNavService } from './ConnectionsAdministrationNavService.js'; @@ -27,18 +25,15 @@ export interface ICreateMethodOptions { @injectable() export class CreateConnectionService { disabled = false; - data: IConnectionFormState | null; + data: ConnectionFormStateRefactored | null; readonly tabsContainer: TabsContainer; constructor( private readonly connectionsAdministrationNavService: ConnectionsAdministrationNavService, private readonly administrationScreenService: AdministrationScreenService, - private readonly connectionFormService: ConnectionFormService, - private readonly connectionInfoResource: ConnectionInfoResource, - private readonly projectsService: ProjectsService, - private readonly projectInfoResource: ProjectInfoResource, - private readonly connectionInfoOriginResource: ConnectionInfoOriginResource, + private readonly serviceProvider: IServiceProvider, + private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, ) { this.data = null; this.tabsContainer = new TabsContainer('Connection Creation mode'); @@ -110,26 +105,20 @@ export class CreateConnectionService { } setConnectionTemplate(projectId: string, config: ConnectionConfig, availableDrivers: string[]): void { - this.data = new ConnectionFormState( - this.projectsService, - this.projectInfoResource, - this.connectionFormService, - this.connectionInfoResource, - this.connectionInfoOriginResource, - ); + this.data = new ConnectionFormStateRefactored(this.serviceProvider, this.connectionFormServiceRefactored, { + projectId, + availableDrivers, + config, + submitType: null, + type: 'admin', + }); - this.data.closeTask.addHandler(this.cancelCreate.bind(this)); - - this.data - .setOptions('create', 'admin') - .setConfig(projectId, config) - .setAvailableDrivers(availableDrivers || []); - - this.data.load(); + // this.data.closeTask.addHandler(this.cancelCreate.bind(this)); } clearConnectionTemplate(): void { - this.data?.dispose(); + // TODO dispose it once we have this API + // this.data?.dispose(); this.data = null; } diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx index 03677e20ac..c3413fb466 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx +++ b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx @@ -23,25 +23,31 @@ import { useS, useTranslate, } from '@cloudbeaver/core-blocks'; -import { isCloudConnection } from '@cloudbeaver/core-connections'; +import { ConnectionInfoOriginResource, ConnectionInfoResource, createConnectionParam, isCloudConnection } from '@cloudbeaver/core-connections'; import type { TLocalizationToken } from '@cloudbeaver/core-localization'; import { CachedMapAllKey, CachedResourceOffsetPageListKey } from '@cloudbeaver/core-resource'; import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '@cloudbeaver/plugin-connections'; +import type { ConnectionFormRefactoredProps } from '@cloudbeaver/plugin-connections'; import styles from './ConnectionAccess.module.css'; import { ConnectionAccessGrantedList } from './ConnectionAccessGrantedList.js'; import { ConnectionAccessList } from './ConnectionAccessList.js'; -import { useConnectionAccessState } from './useConnectionAccessState.js'; +import { useService } from '@cloudbeaver/core-di'; +import { getConnectionFormAccessPart } from './getConnectionFormAccessPart.js'; + +export const ConnectionAccess: TabContainerPanelComponent = observer(function ConnectionAccess({ tabId, formState }) { + const connectionInfoService = useService(ConnectionInfoResource); + const connectionOriginInfoService = useService(ConnectionInfoOriginResource); + const info = connectionInfoService.get(createConnectionParam(formState.state.projectId, formState.state.config.connectionId!)); + const originInfo = connectionOriginInfoService.get(createConnectionParam(formState.state.projectId, formState.state.config.connectionId!)); + const accessPart = getConnectionFormAccessPart(formState); -export const ConnectionAccess: TabContainerPanelComponent = observer(function ConnectionAccess({ tabId, state: formState }) { - const state = useConnectionAccessState(formState.info); const translate = useTranslate(); const style = useS(styles); const { selected } = useTab(tabId); - useAutoLoad(ConnectionAccess, state, selected); + useAutoLoad(ConnectionAccess, [accessPart], selected); const users = useResource(ConnectionAccess, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setParent(UsersResourceFilterKey()), { active: selected, @@ -49,32 +55,32 @@ export const ConnectionAccess: TabContainerPanelComponent const teams = useResource(ConnectionAccess, TeamsResource, CachedMapAllKey, { active: selected }); const grantedUsers = useMemo( - () => computed(() => users.resource.values.filter(user => state.state.grantedSubjects.includes(user.userId))), - [state.state.grantedSubjects, users.resource], + () => computed(() => users.resource.values.filter(user => accessPart.state.grantedSubjects.includes(user.userId))), + [accessPart.state.grantedSubjects, users.resource], ); const grantedTeams = useMemo( - () => computed(() => teams.resource.values.filter(team => state.state.grantedSubjects.includes(team.teamId))), - [state.state.grantedSubjects, teams.resource], + () => computed(() => teams.resource.values.filter(team => accessPart.state.grantedSubjects.includes(team.teamId))), + [accessPart.state.grantedSubjects, teams.resource], ); if (!selected) { return null; } - const loading = users.isLoading() || teams.isLoading() || state.state.loading; - const cloud = formState.info && formState.originInfo?.origin ? isCloudConnection(formState.originInfo.origin) : false; - const disabled = loading || !state.state.loaded || formState.disabled || cloud; - let info: TLocalizationToken | null = null; + const loading = users.isLoading() || teams.isLoading() || accessPart.isLoading(); + const cloud = info && originInfo?.origin ? isCloudConnection(originInfo.origin) : false; + const disabled = loading || !accessPart.isLoaded() || formState.isDisabled || cloud; + let infoToken: TLocalizationToken = ''; - if (formState.mode === 'edit' && state.changed) { - info = 'ui_save_reminder'; + if (formState.mode === 'edit' && accessPart.isChanged) { + infoToken = 'ui_save_reminder'; } else if (cloud) { - info = 'cloud_connections_access_placeholder'; + infoToken = 'cloud_connections_access_placeholder'; } return ( - + {() => ( {!users.resource.values.length && !teams.resource.values.length ? ( @@ -83,22 +89,22 @@ export const ConnectionAccess: TabContainerPanelComponent ) : ( <> - {info && } + {info && } - {state.state.editing && ( + {accessPart.state.editing && ( )} diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts index a0f9cd1543..499a9889b5 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts +++ b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccessTabService.ts @@ -8,22 +8,11 @@ import React from 'react'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { ConnectionInfoResource, createConnectionParam, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { executorHandlerFilter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects'; import { EAdminPermission, PermissionsService } from '@cloudbeaver/core-root'; -import { formStateContext } from '@cloudbeaver/core-ui'; -import type { MetadataValueGetter } from '@cloudbeaver/core-utils'; -import { - connectionConfigContext, - ConnectionFormService, - type IConnectionFormProps, - type IConnectionFormState, - type IConnectionFormSubmitData, -} from '@cloudbeaver/plugin-connections'; - -import type { IConnectionAccessTabState } from './IConnectionAccessTabState.js'; +import { ConnectionFormServiceRefactored } from '@cloudbeaver/plugin-connections'; +import { getConnectionFormAccessPart } from './getConnectionFormAccessPart.js'; const ConnectionAccess = React.lazy(async () => { const { ConnectionAccess } = await import('./ConnectionAccess.js'); @@ -35,136 +24,29 @@ export class ConnectionAccessTabService extends Bootstrap { private readonly key: string; constructor( - private readonly connectionFormService: ConnectionFormService, + private readonly ConnectionFormServiceRefactored: ConnectionFormServiceRefactored, private readonly administrationScreenService: AdministrationScreenService, - private readonly connectionInfoResource: ConnectionInfoResource, private readonly permissionsResource: PermissionsService, private readonly projectInfoResource: ProjectInfoResource, - // private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, ) { super(); this.key = 'access'; } override register(): void { - this.connectionFormService.tabsContainer.add({ + this.ConnectionFormServiceRefactored.parts.add({ key: this.key, name: 'connections_connection_edit_access', title: 'connections_connection_edit_access', order: 4, - stateGetter: context => this.stateGetter(context), - isHidden: (_, context) => !context || !this.isAccessTabActive(context.state), - isDisabled: (tabId, props) => !props?.state.config.driverId || this.administrationScreenService.isConfigurationMode, + stateGetter: context => () => getConnectionFormAccessPart(context.formState), + isHidden: (_, context) => !context || !this.isAccessTabActive(context.formState.state.projectId), + isDisabled: (tabId, props) => !props?.formState.state.config.driverId || this.administrationScreenService.isConfigurationMode, panel: () => ConnectionAccess, }); - - // TODO uncomment it once refactored - // this.connectionFormServiceRefactored.parts.add({ - // key: this.key, - // name: 'connections_connection_edit_access', - // title: 'connections_connection_edit_access', - // order: 4, - // stateGetter: props => () => getConnectionFormAccessPart(props.formState), - // isHidden: (_, context) => !context || !this.isAccessTabActive(context.formState.state.projectId), - // isDisabled: (tabId, props) => !props?.formState.state.driverId || this.administrationScreenService.isConfigurationMode, - // panel: () => ConnectionAccess, - // }); - - this.connectionFormService.formSubmittingTask.addHandler(executorHandlerFilter(data => this.isAccessTabActive(data.state), this.save.bind(this))); - - this.connectionFormService.formStateTask.addHandler(executorHandlerFilter(this.isAccessTabActive.bind(this), this.formState.bind(this))); } - private isAccessTabActive(state: IConnectionFormState): boolean { - return ( - state.projectId !== null && - isGlobalProject(this.projectInfoResource.get(state.projectId)) && - this.permissionsResource.has(EAdminPermission.admin) - ); - } - - private stateGetter(context: IConnectionFormProps): MetadataValueGetter { - return () => ({ - loading: false, - loaded: false, - editing: false, - grantedSubjects: [], - initialGrantedSubjects: [], - }); - } - - private async save(data: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - if (data.submitType === 'test' || !data.state.projectId) { - return; - } - const status = contexts.getContext(this.connectionFormService.connectionStatusContext); - - if (!status.saved) { - return; - } - - const config = contexts.getContext(connectionConfigContext); - const state = this.connectionFormService.tabsContainer.getTabState(data.state.partsState, this.key, { - state: data.state, - }); - - if (!config.connectionId || !state.loaded) { - return; - } - - const key = createConnectionParam(data.state.projectId, config.connectionId); - - const currentGrantedSubjects = await this.connectionInfoResource.loadAccessSubjects(key); - const currentGrantedSubjectIds = currentGrantedSubjects.map(subject => subject.subjectId); - - const { subjectsToRevoke, subjectsToGrant } = this.getSubjectDifferences(currentGrantedSubjectIds, state.grantedSubjects); - - if (subjectsToRevoke.length === 0 && subjectsToGrant.length === 0) { - return; - } - - if (subjectsToRevoke.length > 0) { - await this.connectionInfoResource.deleteConnectionsAccess(key, subjectsToRevoke); - } - - if (subjectsToGrant.length > 0) { - await this.connectionInfoResource.addConnectionsAccess(key, subjectsToGrant); - } - - state.initialGrantedSubjects = state.grantedSubjects.slice(); - } - - private async formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const state = this.connectionFormService.tabsContainer.getTabState(data.partsState, this.key, { state: data }); - - if (!config.connectionId || !data.projectId || !state.loaded) { - return; - } - - const key = createConnectionParam(data.projectId, config.connectionId); - const changed = await this.isChanged(key, state.grantedSubjects); - - if (changed) { - const stateContext = contexts.getContext(formStateContext); - - stateContext.markEdited(); - } - } - - private async isChanged(connectionKey: IConnectionInfoParams, next: string[]): Promise { - const current = await this.connectionInfoResource.loadAccessSubjects(connectionKey); - if (current.length !== next.length) { - return true; - } - - return current.some(value => !next.some(subjectId => subjectId === value.subjectId)); - } - - private getSubjectDifferences(current: string[], next: string[]): { subjectsToRevoke: string[]; subjectsToGrant: string[] } { - const subjectsToRevoke = current.filter(subjectId => !next.includes(subjectId)); - const subjectsToGrant = next.filter(subjectId => !current.includes(subjectId)); - - return { subjectsToRevoke, subjectsToGrant }; + private isAccessTabActive(projectId: string | null): boolean { + return projectId !== null && isGlobalProject(this.projectInfoResource.get(projectId)) && this.permissionsResource.has(EAdminPermission.admin); } } diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionFormAccessPart.ts b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionFormAccessPart.ts index f3d5a81307..46e90e5232 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionFormAccessPart.ts +++ b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionFormAccessPart.ts @@ -10,10 +10,12 @@ import type { IConnectionFormStateRefactored } from '../../../../plugin-connecti import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import type { IConnectionFormAccessState } from './IConnectionFormAccessState.js'; import { createConnectionParam, type ConnectionInfoResource } from '@cloudbeaver/core-connections'; +import { action, makeObservable } from 'mobx'; const getDefaultState = () => ({ grantedSubjects: [], + editing: false, }) as IConnectionFormAccessState; export class ConnectionFormAccessPart extends FormPart { @@ -22,13 +24,20 @@ export class ConnectionFormAccessPart extends FormPart { - const connectionId = this.formState.state.connectionId; + const connectionId = this.formState.state.config.connectionId; const projectId = this.formState.state.projectId; if (!connectionId || !projectId || !this.loaded) { + this.setInitialState(getDefaultState()); return; } @@ -46,7 +55,7 @@ export class ConnectionFormAccessPart extends FormPart>, ): Promise { const status = contexts.getContext(formStatusContext); - const connectionId = this.formState.state.connectionId; + const connectionId = this.formState.state.config.connectionId; if (this.formState.state.submitType === 'test' || !data.state.projectId || !status.saved || !connectionId || !this.loaded) { return; @@ -71,6 +80,18 @@ export class ConnectionFormAccessPart extends FormPart !subjectIds.includes(subject)); + } + + grant(subjectIds: string[]) { + this.state.grantedSubjects.push(...subjectIds); + } } function getSubjectDifferences(current: string[], next: string[]): { subjectsToRevoke: string[]; subjectsToGrant: string[] } { diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/IConnectionFormAccessState.ts b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/IConnectionFormAccessState.ts index 77b37933a1..620a4f6898 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/IConnectionFormAccessState.ts +++ b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/IConnectionFormAccessState.ts @@ -9,6 +9,7 @@ import { schema } from '@cloudbeaver/core-utils'; const CONNECTION_FORM_ACCESS_SCHEMA = schema.object({ grantedSubjects: schema.array(schema.string()), + editing: schema.boolean(), }); export type IConnectionFormAccessState = schema.infer; diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts deleted file mode 100644 index 9e3202fbdc..0000000000 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { action, computed, observable } from 'mobx'; - -import { useObservableRef } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk'; -import { useTabState } from '@cloudbeaver/core-ui'; -import { type ILoadableState, isArraysEqual, isContainsException } from '@cloudbeaver/core-utils'; - -import type { IConnectionAccessTabState } from './IConnectionAccessTabState.js'; - -interface State extends ILoadableState { - state: IConnectionAccessTabState; - changed: boolean; - edit: () => void; - revoke: (subjectIds: string[]) => void; - grant: (subjectIds: string[]) => void; - load: () => Promise; -} - -export function useConnectionAccessState(connection: DatabaseConnectionFragment | undefined): Readonly { - const resource = useService(ConnectionInfoResource); - const notificationService = useService(NotificationService); - const state = useTabState(); - - return useObservableRef( - () => ({ - exception: null as Error | null, - get changed() { - return !isArraysEqual(this.state.initialGrantedSubjects, this.state.grantedSubjects); - }, - isLoading() { - return this.state.loading; - }, - isError() { - return isContainsException(this.exception); - }, - isLoaded() { - return this.state.loaded; - }, - edit() { - this.state.editing = !this.state.editing; - }, - revoke(subjectIds: string[]) { - this.state.grantedSubjects = this.state.grantedSubjects.filter(subject => !subjectIds.includes(subject)); - }, - grant(subjectIds: string[]) { - this.state.grantedSubjects.push(...subjectIds); - }, - async load(reload = false) { - let loaded = this.exception || this.state.loaded; - - if (reload) { - loaded = false; - } - - if (loaded || this.state.loading) { - return; - } - - try { - this.state.loading = true; - - if (this.connection) { - const key = createConnectionParam(this.connection); - const grantedSubjects = await this.resource.loadAccessSubjects(key); - this.state.grantedSubjects = grantedSubjects.map(subject => subject.subjectId); - this.state.initialGrantedSubjects = this.state.grantedSubjects.slice(); - } - - this.state.loaded = true; - this.exception = null; - } catch (exception: any) { - this.notificationService.logException(exception, 'connections_connection_edit_access_load_failed'); - this.exception = exception; - } finally { - this.state.loading = false; - } - }, - async reload() { - this.load(true); - }, - }), - { - exception: observable.ref, - state: observable.ref, - changed: computed, - edit: action.bound, - isLoading: action.bound, - isLoaded: action.bound, - reload: action.bound, - revoke: action.bound, - grant: action.bound, - }, - { state, connection, resource, notificationService }, - ['load'], - ); -} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.tsx index 73c66567e3..5ae595547b 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionForm.tsx @@ -6,22 +6,22 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; import { ExceptionMessage, Form, Loader, Placeholder, s, StatusMessage, useExecutor, useForm, useObjectRef, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { ENotificationType } from '@cloudbeaver/core-events'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; -import { TabList, TabPanelList, TabsState } from '@cloudbeaver/core-ui'; +import { formStatusContext, formValidationContext, TabList, TabPanelList, TabsState } from '@cloudbeaver/core-ui'; import { ConnectionFormActionsContext, type IConnectionFormActionsContext } from './ConnectFormActionsContext.js'; import style from './ConnectionForm.module.css'; -import { ConnectionFormService } from './ConnectionFormService.js'; import { connectionConfigContext } from './Contexts/connectionConfigContext.js'; -import type { IConnectionFormState } from './IConnectionFormProps.js'; +import type { ConnectionFormStateRefactored } from './ConnectionFormStateRefactored.js'; +import { getFirstException } from '@cloudbeaver/core-utils'; +import { ConnectionFormServiceRefactored } from './ConnectionFormServiceRefactored.js'; export interface ConnectionFormProps { - state: IConnectionFormState; + state: ConnectionFormStateRefactored; onCancel?: () => void; onSave?: (config: ConnectionConfig) => void; className?: string; @@ -29,15 +29,21 @@ export interface ConnectionFormProps { export const ConnectionForm = observer(function ConnectionForm({ state, onCancel, onSave = () => {}, className }) { const props = useObjectRef({ onSave }); - const service = useService(ConnectionFormService); + const service = useService(ConnectionFormServiceRefactored); const styles = useS(style); const form = useForm({ onSubmit: event => { if (event?.type === 'test') { - state.test(); + state.setState({ + ...state.state, + submitType: 'test', + }); } else { - state.save(); + state.setState({ + ...state.state, + submitType: 'submit', + }); } }, }); @@ -48,51 +54,48 @@ export const ConnectionForm = observer(function ConnectionF })); useExecutor({ - executor: state.submittingTask, + executor: state.submitTask, postHandlers: [ function save(data, contexts) { - const validation = contexts.getContext(service.connectionValidationContext); - const state = contexts.getContext(service.connectionStatusContext); + const validation = contexts.getContext(formValidationContext); + const state = contexts.getContext(formStatusContext); const config = contexts.getContext(connectionConfigContext); - if (validation.valid && state.saved && data.submitType === 'submit') { + if (validation.valid && state.saved && data.state.submitType === 'submit') { props.onSave(config); } }, ], }); - useEffect(() => { - state.loadConnectionInfo(); - }, [state]); - - if (state.initError) { - return state.loadConnectionInfo()} />; + if (getFirstException(state.exception)) { + return ; } - if (!state.configured) { - return ( -
- -
- ); - } + // should we delete it? + // if (!state.configured) { + // return ( + //
+ // + //
+ // ); + // } return (
- +
- +
- +
diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormService.ts deleted file mode 100644 index bc66406537..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormService.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { observable, runInAction, toJS } from 'mobx'; - -import { PlaceholderContainer } 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, type IExecutorHandler, type IExecutorHandlersCollection } from '@cloudbeaver/core-executor'; -import { LocalizationService } from '@cloudbeaver/core-localization'; -import { TabsContainer } from '@cloudbeaver/core-ui'; - -import { ConnectionAuthenticationDialogLoader } from '../ConnectionAuthentication/ConnectionAuthenticationDialogLoader.js'; -import { ConnectionFormBaseActionsLoader } from './ConnectionFormBaseActionsLoader.js'; -import { connectionConfigContext } from './Contexts/connectionConfigContext.js'; -import { connectionCredentialsStateContext } from './Contexts/connectionCredentialsStateContext.js'; -import type { IConnectionFormFillConfigData, IConnectionFormProps, IConnectionFormState, IConnectionFormSubmitData } from './IConnectionFormProps.js'; - -export interface IConnectionFormValidation { - valid: boolean; - messages: string[]; - info: (message: string) => void; - error: (message: string) => void; -} - -export interface IConnectionFormStatus { - saved: boolean; - messages: string[]; - exception: Error | null; - info: (message: string) => void; - error: (message: string, exception?: Error) => void; -} - -@injectable() -export class ConnectionFormService { - readonly tabsContainer: TabsContainer; - readonly actionsContainer: PlaceholderContainer; - - readonly configureTask: IExecutorHandlersCollection; - readonly fillConfigTask: IExecutorHandlersCollection; - readonly prepareConfigTask: IExecutorHandlersCollection; - readonly formValidationTask: IExecutorHandlersCollection; - readonly formSubmittingTask: IExecutorHandlersCollection; - readonly formStateTask: IExecutorHandlersCollection; - - constructor( - private readonly notificationService: NotificationService, - private readonly commonDialogService: CommonDialogService, - private readonly localizationService: LocalizationService, - ) { - this.tabsContainer = new TabsContainer('Connection settings'); - this.actionsContainer = new PlaceholderContainer(); - this.configureTask = new ExecutorHandlersCollection(); - this.fillConfigTask = new ExecutorHandlersCollection(); - this.prepareConfigTask = new ExecutorHandlersCollection(); - this.formSubmittingTask = new ExecutorHandlersCollection(); - this.formValidationTask = new ExecutorHandlersCollection(); - this.formStateTask = new ExecutorHandlersCollection(); - - this.formSubmittingTask.before(this.formValidationTask).before(this.prepareConfigTask); - - this.formStateTask.before(this.prepareConfigTask, state => ({ state, submitType: 'submit' })); - - this.prepareConfigTask.addPostHandler(this.askCredentials); - this.formSubmittingTask.addPostHandler(this.showSubmittingStatusMessage); - this.formValidationTask.addPostHandler(this.ensureValidation); - - this.actionsContainer.add(ConnectionFormBaseActionsLoader); - } - - connectionValidationContext = (): IConnectionFormValidation => ({ - valid: true, - messages: [], - info(message: string) { - this.messages.push(message); - }, - error(message: string) { - this.messages.push(message); - this.valid = false; - }, - }); - - connectionStatusContext = (): IConnectionFormStatus => ({ - saved: true, - messages: [], - exception: null, - info(message: string) { - this.messages.push(message); - }, - error(message: string, exception: Error | null = null) { - this.messages.push(message); - this.saved = false; - this.exception = exception; - }, - }); - - private readonly showSubmittingStatusMessage: IExecutorHandler = (data, contexts) => { - const status = contexts.getContext(this.connectionStatusContext); - - if (!status.saved) { - ExecutorInterrupter.interrupt(contexts); - } - - if (status.messages.length > 0) { - if (status.exception) { - this.notificationService.logException(status.exception, status.messages[0], status.messages.slice(1).join('\n')); - } else { - this.notificationService.notify( - { - title: status.messages[0]!, - message: status.messages.slice(1).join('\n'), - }, - status.saved ? ENotificationType.Success : ENotificationType.Error, - ); - } - } - }; - - private readonly askCredentials: IExecutorHandler = async (data, contexts) => { - const credentialsState = contexts.getContext(connectionCredentialsStateContext); - - if (data.submitType !== 'test' || (!credentialsState.authModelId && !credentialsState.networkHandlers.length)) { - return; - } - - const config = contexts.getContext(connectionConfigContext); - - runInAction(() => { - if (credentialsState.authModelId) { - if (!config.credentials) { - config.credentials = { ...data.state.config.credentials }; - } - - config.credentials = observable(config.credentials); - } - - if (credentialsState.networkHandlers.length > 0) { - if (!config.networkHandlersConfig) { - config.networkHandlersConfig = toJS(data.state.config.networkHandlersConfig) || []; - } - - config.networkHandlersConfig = observable(config.networkHandlersConfig); - } - }); - - const result = await this.commonDialogService.open(ConnectionAuthenticationDialogLoader, { - config, - authModelId: credentialsState.authModelId, - networkHandlers: credentialsState.networkHandlers, - projectId: data.state.projectId, - }); - - if (result === DialogueStateResult.Rejected) { - ExecutorInterrupter.interrupt(contexts); - } - }; - - private readonly ensureValidation: IExecutorHandler = (data, contexts) => { - const validation = contexts.getContext(this.connectionValidationContext); - - if (!validation.valid) { - ExecutorInterrupter.interrupt(contexts); - } - - if (validation.messages.length > 0) { - const messages = validation.messages.map(message => this.localizationService.translate(message)); - this.notificationService.notify( - { - title: - data.state.mode === 'edit' ? 'connections_administration_connection_save_error' : 'connections_administration_connection_create_error', - message: messages.join('\n'), - }, - validation.valid ? ENotificationType.Info : ENotificationType.Error, - ); - } - }; -} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts deleted file mode 100644 index 3e4b8af8db..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormState.ts +++ /dev/null @@ -1,342 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { action, computed, makeObservable, observable } from 'mobx'; - -import { - ConnectionInfoOriginResource, - ConnectionInfoResource, - createConnectionParam, - type DatabaseConnection, - type IConnectionInfoParams, -} from '@cloudbeaver/core-connections'; -import { Executor, type IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor'; -import type { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; -import type { ResourceKeySimple } from '@cloudbeaver/core-resource'; -import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; -import { formStateContext, type IFormStateInfo } from '@cloudbeaver/core-ui'; -import { MetadataMap, uuid } from '@cloudbeaver/core-utils'; - -import { connectionFormConfigureContext } from './connectionFormConfigureContext.js'; -import type { ConnectionFormService } from './ConnectionFormService.js'; -import type { ConnectionFormMode, ConnectionFormType, IConnectionFormState, IConnectionFormSubmitData } from './IConnectionFormProps.js'; - -export class ConnectionFormState implements IConnectionFormState { - mode: ConnectionFormMode; - type: ConnectionFormType; - projectId: string | null; - - config: ConnectionConfig; - - partsState: MetadataMap; - - statusMessage: string | string[] | null; - configured: boolean; - initError: Error | null; - - get loading(): boolean { - return this.loadConnectionTask.executing || this.submittingTask.executing; - } - - get disabled(): boolean { - return this.loading || !!this.stateInfo?.disabled || this.loadConnectionTask.executing; - } - - get availableDrivers(): string[] { - if (this._availableDrivers.length === 0 && this.config.driverId) { - return [this.config.driverId]; - } - - return this._availableDrivers; - } - - get info(): DatabaseConnection | undefined { - if (!this.config.connectionId || this.projectId === null) { - return undefined; - } - - return this.resource.get(createConnectionParam(this.projectId, this.config.connectionId)); - } - - get originInfo() { - if (!this.config.connectionId || this.projectId === null) { - return undefined; - } - - return this.originResource.get(createConnectionParam(this.projectId, this.config.connectionId)); - } - - get readonly(): boolean { - if (this.stateInfo?.readonly) { - return true; - } - - if (this.type === 'admin' || this.mode === 'create') { - return false; - } - - if (!this.info?.canEdit) { - return true; - } - - return false; - } - - get id(): string { - if (this.mode === 'create') { - return 'create'; - } - - return this.config.connectionId || this._id; - } - - readonly resource: ConnectionInfoResource; - readonly originResource: ConnectionInfoOriginResource; - readonly service: ConnectionFormService; - readonly submittingTask: IExecutor; - readonly closeTask: IExecutor; - - private readonly _id: string; - private stateInfo: IFormStateInfo | null; - private readonly loadConnectionTask: IExecutor; - private readonly formStateTask: IExecutor; - private _availableDrivers: string[]; - - constructor( - private readonly projectsService: ProjectsService, - private readonly projectInfoResource: ProjectInfoResource, - service: ConnectionFormService, - resource: ConnectionInfoResource, - originResource: ConnectionInfoOriginResource, - ) { - this._id = uuid(); - this.initError = null; - - this.resource = resource; - this.originResource = originResource; - this.projectId = null; - this.config = {}; - this._availableDrivers = []; - this.stateInfo = null; - this.partsState = new MetadataMap(); - this.service = service; - this.formStateTask = new Executor(this, () => true); - this.loadConnectionTask = new Executor(this, () => true); - this.submittingTask = new Executor(); - this.closeTask = new Executor(); - this.statusMessage = null; - this.configured = false; - this.mode = 'create'; - this.type = 'public'; - - this.syncProject = this.syncProject.bind(this); - this.syncInfo = this.syncInfo.bind(this); - this.test = this.test.bind(this); - this.save = this.save.bind(this); - this.checkFormState = this.checkFormState.bind(this); - this.loadInfo = this.loadInfo.bind(this); - this.updateFormState = this.updateFormState.bind(this); - - this.formStateTask.addCollection(service.formStateTask).addPostHandler(this.updateFormState); - - this.resource.onItemUpdate.addHandler(this.syncInfo); - - this.projectInfoResource.onDataUpdate.addHandler(this.syncProject); - - this.projectsService.onActiveProjectChange.addHandler(this.syncProject); - - this.submittingTask.addPostHandler(async (data, contexts) => { - const status = contexts.getContext(service.connectionStatusContext); - const validation = contexts.getContext(service.connectionValidationContext); - - if (data.submitType !== 'submit' || !status.saved || !validation.valid) { - return; - } - - this.reset(); - await this.load(); - }); - - this.loadConnectionTask - .before(service.configureTask) - .addPostHandler(this.loadInfo) - .next(service.fillConfigTask, (state, contexts) => { - const configuration = contexts.getContext(connectionFormConfigureContext); - - return { - state, - updated: state.info !== configuration.info || state.config.driverId !== configuration.driverId || !this.configured, - }; - }) - .next(this.formStateTask); - - makeObservable(this, { - projectId: observable, - mode: observable, - type: observable, - config: observable, - availableDrivers: computed, - _availableDrivers: observable, - info: computed, - originInfo: computed, - statusMessage: observable, - configured: observable, - readonly: computed, - stateInfo: observable, - initError: observable.ref, - id: computed, - reset: action, - setOptions: action, - setConfig: action, - setAvailableDrivers: action, - updateFormState: action, - }); - } - - async loadConnectionInfo(): Promise { - try { - this.initError = null; - await this.loadConnectionTask.execute(this); - - return this.info; - } catch (exception: any) { - this.initError = exception; - throw exception; - } - } - - async load(): Promise { - await this.loadConnectionInfo(); - } - - reset(): void { - this.configured = false; - this.partsState.clear(); - } - - setPartsState(state: MetadataMap): this { - this.partsState = state; - return this; - } - - setOptions(mode: ConnectionFormMode, type: ConnectionFormType): this { - this.mode = mode; - this.type = type; - return this; - } - - setConfig(projectId: string, config: ConnectionConfig): this { - this.setProject(projectId); - this.config = config; - this.reset(); - return this; - } - - setProject(projectId: string): this { - this.projectId = projectId; - return this; - } - - setAvailableDrivers(drivers: string[]): this { - this._availableDrivers = drivers; - this.reset(); - return this; - } - - async save(): Promise { - await this.submittingTask.executeScope( - { - state: this, - submitType: 'submit', - }, - this.service.formSubmittingTask, - ); - } - - async test(): Promise { - await this.submittingTask.executeScope( - { - state: this, - submitType: 'test', - }, - this.service.formSubmittingTask, - ); - } - - async checkFormState(): Promise { - await this.loadConnectionInfo(); - return this.stateInfo; - } - - dispose(): void { - this.resource.onItemUpdate.removeHandler(this.syncInfo); - this.projectInfoResource.onDataUpdate.removeHandler(this.syncProject); - this.projectsService.onActiveProjectChange.removeHandler(this.syncProject); - } - - async close(): Promise { - await this.closeTask.execute(); - } - - private updateFormState(data: IConnectionFormState, contexts: IExecutionContextProvider): void { - const context = contexts.getContext(formStateContext); - - if (this.mode === 'create') { - context.markEdited(); - } - - this.statusMessage = context.statusMessage; - - if (this.statusMessage === null && this.mode === 'edit') { - if (!this.info?.canEdit) { - this.statusMessage = 'connections_connection_edit_not_own_deny'; - } - } - - this.stateInfo = context; - this.configured = true; - } - - private syncInfo(key: ResourceKeySimple) { - if ( - !this.config.connectionId || - this.projectId === null || - !this.resource.isIntersect(key, createConnectionParam(this.projectId, this.config.connectionId)) - ) { - return; - } - - this.loadConnectionInfo(); - } - - private async syncProject() { - if (!this.projectId) { - return; - } - - const project = this.projectInfoResource.get(this.projectId); - if (!project?.canEditDataSources || !this.projectsService.activeProjects.includes(project)) { - await this.close(); - } - } - - private async loadInfo(data: IConnectionFormState, contexts: IExecutionContextProvider) { - if (!data.config.connectionId || data.projectId === null) { - return; - } - - const key = createConnectionParam(data.projectId, data.config.connectionId); - const configuration = contexts.getContext(connectionFormConfigureContext); - - if (!data.resource.has(key)) { - return; - } - - await data.resource.load(key, configuration.connectionIncludes); - await this.originResource.load(key); - } -} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormStateRefactored.ts b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormStateRefactored.ts index 98e6e0b318..37ada49248 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormStateRefactored.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormStateRefactored.ts @@ -8,7 +8,7 @@ import type { IServiceProvider } from '@cloudbeaver/core-di'; import { FormState } from '@cloudbeaver/core-ui'; -import type { ConnectionFormServiceRefactored } from './ConnectionFormServiceRefactored.js'; +import { ConnectionFormServiceRefactored } from './ConnectionFormServiceRefactored.js'; import type { IConnectionFormStateRefactored } from './IConnectionFormStateRefactored.js'; export class ConnectionFormStateRefactored extends FormState { diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts index d8d9b26c4e..64522508b7 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts @@ -5,140 +5,31 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, makeObservable } from 'mobx'; -import { DBDriverResource } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { isObjectPropertyInfoStateEqual } from '@cloudbeaver/core-sdk'; -import { formStateContext } from '@cloudbeaver/core-ui'; -import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js'; -import { ConnectionFormService } from '../ConnectionFormService.js'; -import { connectionConfigContext } from '../Contexts/connectionConfigContext.js'; -import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps.js'; import { DriverPropertiesLoader } from './DriverPropertiesLoader.js'; +import { ConnectionFormServiceRefactored } from '../ConnectionFormServiceRefactored.js'; @injectable() export class ConnectionDriverPropertiesTabService extends Bootstrap { - constructor( - private readonly connectionFormService: ConnectionFormService, - private readonly dbDriverResource: DBDriverResource, - // private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, - ) { + constructor(private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored) { super(); - - makeObservable(this, { - fillConfig: action, - }); } override register(): void { - this.connectionFormService.tabsContainer.add({ + this.connectionFormServiceRefactored.parts.add({ key: 'driver_properties', name: 'plugin_connections_connection_form_part_properties', title: 'plugin_connections_connection_form_part_properties', order: 2, panel: () => DriverPropertiesLoader, isDisabled: (tabId, props) => { - if (props?.state.config.driverId) { - return !props.state.config.driverId; + if (props?.formState.state.config.driverId) { + return !props.formState.state.config.driverId; } return true; }, }); - - // TODO uncomment once refactored - // this.connectionFormServiceRefactored.parts.add({ - // key: 'driver_properties', - // name: 'plugin_connections_connection_form_part_properties', - // title: 'plugin_connections_connection_form_part_properties', - // order: 2, - // panel: () => DriverPropertiesLoader, - // isDisabled: (tabId, props) => { - // if (!props?.formState) { - // return true; - // } - - // if (props.formState.state.driverId) { - // return false; - // } - - // return true; - // }, - // }); - - this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)); - - this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - - this.connectionFormService.configureTask.addHandler(this.configure.bind(this)); - } - - // TODO include this - private configure(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const configuration = contexts.getContext(connectionFormConfigureContext); - - configuration.include('includeProperties', 'includeProviderProperties'); - } - - private fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider) { - if (!updated) { - return; - } - if (!state.config.properties) { - state.config.properties = {}; - } - - if (!state.info) { - return; - } - - state.config.properties = { ...state.info.properties }; - } - - private prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - - config.properties = { ...state.config.properties }; - - if (config.driverId) { - const driver = this.dbDriverResource.get(config.driverId); - const trimmedProperties: typeof config.properties = {}; - - const defaultDriverProperties = new Set(driver?.driverProperties?.map(property => property.id) ?? []); - - for (let key of Object.keys(config.properties)) { - const value = config.properties[key]; - if (!defaultDriverProperties?.has(key)) { - key = key.trim(); - } - - trimmedProperties[key] = typeof value === 'string' ? value.trim() : value; - } - - config.properties = trimmedProperties; - } - } - - private formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - if (!data.info || !data.config.driverId) { - return; - } - - const config = contexts.getContext(connectionConfigContext); - const driver = this.dbDriverResource.get(data.config.driverId); - - if (!driver?.driverProperties) { - return; - } - - if (!isObjectPropertyInfoStateEqual(driver.driverProperties, config.properties, data.info.properties)) { - const stateContext = contexts.getContext(formStateContext); - - stateContext.markEdited(); - } } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionFormDriverPropertiesPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionFormDriverPropertiesPart.ts index c05f02dcd1..ffd552a887 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionFormDriverPropertiesPart.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionFormDriverPropertiesPart.ts @@ -8,8 +8,8 @@ import { FormPart, type IFormState } from '@cloudbeaver/core-ui'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import type { DBDriverResource } from '@cloudbeaver/core-connections'; -import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; import type { IConnectionFormStateRefactored } from '../IConnectionFormStateRefactored.js'; +import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js'; export class ConnectionFormDriverPropertiesPart extends FormPart { constructor( @@ -21,6 +21,15 @@ export class ConnectionFormDriverPropertiesPart extends FormPart {} + protected override configure( + data: IFormState, + contexts: IExecutionContextProvider>, + ): void | Promise { + const configuration = contexts.getContext(connectionFormConfigureContext); + + configuration.include('includeProperties', 'includeProviderProperties'); + } + protected override async saveChanges( data: IFormState, contexts: IExecutionContextProvider>, @@ -30,15 +39,14 @@ export class ConnectionFormDriverPropertiesPart extends FormPart, contexts: IExecutionContextProvider>, ): void | Promise { - const driverId = this.formState.state.driverId; - const config = getConnectionFormOptionsPart(this.formState).state; + const config = this.formState.state.config; if (!config.properties) { config.properties = {}; } - if (driverId) { - const driver = this.dbDriverResource.get(driverId); + if (config.driverId) { + const driver = this.dbDriverResource.get(config.driverId); const defaultDriverProperties = new Set(driver?.driverProperties?.map(property => property.id) ?? []); for (let key of Object.keys(config.properties)) { diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx index 957e7e2479..dd5028ac80 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx @@ -9,17 +9,19 @@ import { computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useMemo, useState } from 'react'; -import { ColoredContainer, Group, type IProperty, PropertiesTable, s, useResource, useS } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Group, type IProperty, PropertiesTable, s, useAutoLoad, useResource, useS } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; import { uuid } from '@cloudbeaver/core-utils'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; import styles from './DriverProperties.module.css'; +import type { ConnectionFormRefactoredProps } from '../ConnectionFormServiceRefactored.js'; +import { getConnectionFormDriverPropertiesPart } from './getConnectionFormDriverPropertiesPart.js'; -export const DriverProperties: TabContainerPanelComponent = observer(function DriverProperties({ tabId, state: formState }) { +export const DriverProperties: TabContainerPanelComponent = observer(function DriverProperties({ tabId, formState }) { const { selected } = useTab(tabId); const style = useS(styles); + const driverPropertiesPart = getConnectionFormDriverPropertiesPart(formState); const [state] = useState(() => { const propertiesList: IProperty[] = observable([]); @@ -42,18 +44,20 @@ export const DriverProperties: TabContainerPanelComponent }); const driver = useResource(DriverProperties, DBDriverResource, { - key: (selected && formState.config.driverId) || null, + key: (selected && formState.state.config.driverId) || null, includes: ['includeDriverProperties'] as const, }); + useAutoLoad(DriverProperties, driverPropertiesPart); + runInAction(() => { if (driver.data) { - for (const key of Object.keys(formState.config.properties)) { + for (const key of Object.keys(formState.state.config.properties ?? {})) { if (driver.data.driverProperties.some(property => property.id === key) || state.propertiesList.some(property => property.key === key)) { continue; } - state.add(key, formState.config.properties[key]); + state.add(key, formState.state.config.properties![key]); } } }); @@ -84,8 +88,8 @@ export const DriverProperties: TabContainerPanelComponent ): ConnectionFormDriverPropertiesPart { +export function getConnectionFormDriverPropertiesPart(formState: IFormState): ConnectionFormDriverPropertiesPart { return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_DRIVER_PROPERTIES_PART, context => { const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; const dbDriverResource = di.getService(DBDriverResource); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts index 95993cc023..92f2795db0 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts @@ -59,11 +59,6 @@ export interface IConnectionFormProps { onCancel?: () => void; } -export interface IConnectionFormFillConfigData { - updated: boolean; - state: IConnectionFormState; -} - export interface IConnectionFormSubmitData { submitType: 'submit' | 'test'; state: IConnectionFormState; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormStateRefactored.ts b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormStateRefactored.ts index 95f44676c8..cfe5be9cdd 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormStateRefactored.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormStateRefactored.ts @@ -7,13 +7,15 @@ */ import { schema } from '@cloudbeaver/core-utils'; +import { CONNECTION_CONFIG_SCHEMA } from './Options/IConnectionConfig.js'; export const CONNECTION_FORM_STATE_SCHEMA = schema .object({ projectId: schema.string(), - connectionId: schema.string(), - driverId: schema.string(), - submitType: schema.enum(['submit', 'test']), + config: CONNECTION_CONFIG_SCHEMA, + submitType: schema.enum(['submit', 'test']).nullable(), + availableDrivers: schema.array(schema.string()), + type: schema.enum(['admin', 'public']), }) .required() .strict(); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionFormOpitonsPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionFormOpitonsPart.ts index 2ec7fdf1ed..9f73ce2296 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionFormOpitonsPart.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionFormOpitonsPart.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { FormPart, formStateContext, formStatusContext, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; -import { DriverConfigurationType, isObjectPropertyInfoStateEqual, type ConnectionConfig, type ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { DriverConfigurationType, type ConnectionConfig, type ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { ConnectionInfoProjectKey, @@ -20,13 +20,14 @@ import { import type { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, UserInfoResource } from '@cloudbeaver/core-authentication'; import { toJS } from 'mobx'; -import { getUniqueName, isNotNullDefined, isValuesEqual } from '@cloudbeaver/core-utils'; +import { getUniqueName, isNotNullDefined } from '@cloudbeaver/core-utils'; import { getDefaultConfigurationType } from './getDefaultConfigurationType.js'; import { getConnectionName } from './getConnectionName.js'; import type { LocalizationService } from '@cloudbeaver/core-localization'; import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext.js'; import type { IConnectionFormOptionsState } from './IConnectionFormOptionsState.js'; import type { IConnectionFormStateRefactored } from '../IConnectionFormStateRefactored.js'; +import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js'; const MAIN_PROPERTY_DATABASE_KEY = 'database'; const MAIN_PROPERTY_HOST_KEY = 'host'; @@ -79,51 +80,68 @@ export class ConnectionFormOptionsPart extends FormPart, + contexts: IExecutionContextProvider>, + ): void | Promise { + const configuration = contexts.getContext(connectionFormConfigureContext); - if (!info) { - return super.isChanged; - } - - const driver = this.formState.state.driverId ? this.dbDriverResource.get(this.formState.state.driverId) : undefined; - - return ( - super.isChanged || - // TODO do I need it? - !isValuesEqual(this.state.name, info.name, '') || - !isValuesEqual(this.state.configurationType, info.configurationType, DriverConfigurationType.Manual) || - !isValuesEqual(this.state.description, info.description, '') || - !isValuesEqual(this.state.template, info.template, true) || - !isValuesEqual(this.state.folder, info.folder, undefined) || - !isValuesEqual(this.state.driverId, info.driverId, '') || - (this.state.url !== undefined && !isValuesEqual(this.state.url, info.url, '')) || - (this.state.host !== undefined && !isValuesEqual(this.state.host, info.host, '')) || - (this.state.port !== undefined && !isValuesEqual(this.state.port, info.port, '')) || - (this.state.serverName !== undefined && !isValuesEqual(this.state.serverName, info.serverName, '')) || - (this.state.databaseName !== undefined && !isValuesEqual(this.state.databaseName, info.databaseName, '')) || - this.state.credentials !== undefined || - (this.state.authModelId !== undefined && !isValuesEqual(this.state.authModelId, info.authModel, '')) || - (this.state.saveCredentials !== undefined && this.state.saveCredentials !== info.credentialsSaved) || - (this.state.sharedCredentials !== undefined && this.state.sharedCredentials !== info.sharedCredentials) || - (this.state.providerProperties !== undefined && - !isObjectPropertyInfoStateEqual(driver?.providerProperties ?? [], this.state.providerProperties, info.providerProperties)) || - (this.state.mainPropertyValues !== undefined && - !isObjectPropertyInfoStateEqual(driver?.mainProperties ?? [], this.state.mainPropertyValues, info.mainPropertyValues)) || - (this.state.keepAliveInterval !== undefined && !isValuesEqual(this.state.keepAliveInterval, info.keepAliveInterval)) || - (this.state.autocommit !== undefined && !isValuesEqual(this.state.autocommit, info.autocommit)) - ); + configuration.include('includeAuthProperties', 'includeCredentialsSaved', 'customIncludeOptions'); } + // TODO should we have it? + // override get isChanged(): boolean { + // const info = this.connectionInfoResource.get(createConnectionParam(this.formState.state.projectId, this.formState.state.config.connectionId)); + + // if (!info) { + // return super.isChanged; + // } + + // const driver = this.formState.state.config.driverId ? this.dbDriverResource.get(this.formState.state.driverId) : undefined; + + // return ( + // super.isChanged || + // // TODO do I need it? + // !isValuesEqual(this.state.name, info.name, '') || + // !isValuesEqual(this.state.configurationType, info.configurationType, DriverConfigurationType.Manual) || + // !isValuesEqual(this.state.description, info.description, '') || + // !isValuesEqual(this.state.template, info.template, true) || + // !isValuesEqual(this.state.folder, info.folder, undefined) || + // !isValuesEqual(this.state.driverId, info.driverId, '') || + // (this.state.url !== undefined && !isValuesEqual(this.state.url, info.url, '')) || + // (this.state.host !== undefined && !isValuesEqual(this.state.host, info.host, '')) || + // (this.state.port !== undefined && !isValuesEqual(this.state.port, info.port, '')) || + // (this.state.serverName !== undefined && !isValuesEqual(this.state.serverName, info.serverName, '')) || + // (this.state.databaseName !== undefined && !isValuesEqual(this.state.databaseName, info.databaseName, '')) || + // this.state.credentials !== undefined || + // (this.state.authModelId !== undefined && !isValuesEqual(this.state.authModelId, info.authModel, '')) || + // (this.state.saveCredentials !== undefined && this.state.saveCredentials !== info.credentialsSaved) || + // (this.state.sharedCredentials !== undefined && this.state.sharedCredentials !== info.sharedCredentials) || + // (this.state.providerProperties !== undefined && + // !isObjectPropertyInfoStateEqual(driver?.providerProperties ?? [], this.state.providerProperties, info.providerProperties)) || + // (this.state.mainPropertyValues !== undefined && + // !isObjectPropertyInfoStateEqual(driver?.mainProperties ?? [], this.state.mainPropertyValues, info.mainPropertyValues)) || + // (this.state.keepAliveInterval !== undefined && !isValuesEqual(this.state.keepAliveInterval, info.keepAliveInterval)) || + // (this.state.autocommit !== undefined && !isValuesEqual(this.state.autocommit, info.autocommit)) + // ); + // } + protected override async loader(): Promise { - const info = this.connectionInfoResource.get(createConnectionParam(this.formState.state.projectId, this.formState.state.connectionId)); + const connectionId = this.formState.state.config.connectionId; + const info = connectionId ? this.connectionInfoResource.get(createConnectionParam(this.formState.state.projectId, connectionId)) : undefined; if (!info) { const defaultConnectionConfig = await this.getDefaults(); - this.setInitialState({ + const config = { ...defaultStateGetter(), ...defaultConnectionConfig, - }); + }; + + this.setInitialState(config); + this.formState.state.config = { + ...config, + ...this.formState.state.config, + }; return; } @@ -169,10 +187,8 @@ export class ConnectionFormOptionsPart extends FormPart { - if (!this.state.driverId) { + const driverId = this.formState.state.config.driverId || this.state.driverId; + if (!driverId) { // TODO remove it? throw new Error('Driver id is not provided'); } - const defaultConnectionConfig: ConnectionConfig = { ...this.state }; + const defaultConnectionConfig: ConnectionConfig = { ...this.formState.state.config }; - const driver = await this.dbDriverResource.load(this.state.driverId, ['includeProviderProperties']); + const driver = await this.dbDriverResource.load(driverId, ['includeProviderProperties']); defaultConnectionConfig.authModelId = driver?.defaultAuthModel; defaultConnectionConfig.configurationType = getDefaultConfigurationType(driver); @@ -310,6 +327,7 @@ export class ConnectionFormOptionsPart extends FormPart { const { Options } = await import('./Options.js'); return { default: Options }; }); -const MAIN_PROPERTY_DATABASE_KEY = 'database'; -const MAIN_PROPERTY_HOST_KEY = 'host'; -const MAIN_PROPERTY_PORT_KEY = 'port'; -const MAIN_PROPERTY_SERVER_KEY = 'server'; - @injectable() export class ConnectionOptionsTabService extends Bootstrap { - constructor( - private readonly projectInfoResource: ProjectInfoResource, - private readonly connectionFormService: ConnectionFormService, - private readonly dbDriverResource: DBDriverResource, - private readonly userInfoResource: UserInfoResource, - private readonly localizationService: LocalizationService, - private readonly authProvidersResource: AuthProvidersResource, - private readonly databaseAuthModelsResource: DatabaseAuthModelsResource, - // private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, - ) { + constructor(private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored) { super(); - - makeObservable(this, { - fillConfig: action, - }); } override register(): void { - this.connectionFormService.tabsContainer.add({ + this.connectionFormServiceRefactored.parts.add({ key: 'options', name: 'plugin_connections_connection_form_part_main', order: 1, panel: () => Options, }); - - // TODO uncomment it once Options component is refactored - // this.connectionFormServiceRefactored.parts.add({ - // key: 'options', - // order: 1, - // name: 'plugin_connections_connection_form_part_main', - // panel: () => Options, - // }); - - this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.connectionFormService.formValidationTask.addHandler(this.validate.bind(this)); - - this.connectionFormService.formSubmittingTask.addHandler(this.save.bind(this)); - - this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)).addHandler(this.formAuthState.bind(this)); - - this.connectionFormService.configureTask.addHandler(this.configure.bind(this)); - - this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - } - - private async save({ state, submitType }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const status = contexts.getContext(this.connectionFormService.connectionStatusContext); - const config = contexts.getContext(connectionConfigContext); - - if (!state.projectId) { - status.error('connections_connection_create_fail'); - return; - } - - try { - if (submitType === 'submit') { - if (state.mode === 'edit') { - const connection = await state.resource.update(createConnectionParam(state.projectId, config.connectionId!), config); - status.info('Connection was updated'); - status.info(connection.name); - } else { - const connection = await state.resource.create(state.projectId, config); - config.connectionId = connection.id; - status.info('Connection was created'); - status.info(connection.name); - } - } else { - const info = await state.resource.test(state.projectId, config); - status.info('Connection is established'); - status.info('Client version: ' + info.clientVersion); - status.info('Server version: ' + info.serverVersion); - status.info('Connection time: ' + info.connectTime); - } - } catch (exception: any) { - if (submitType === 'submit') { - status.error('connections_connection_create_fail', exception); - } else { - status.error('connections_connection_test_fail', exception); - } - } - } - - private async validate({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const validation = contexts.getContext(this.connectionFormService.connectionValidationContext); - - if (state.config.configurationType === DriverConfigurationType.Manual && state.config.host?.length === 0 && state.config.driverId) { - const driver = await this.dbDriverResource.load(state.config.driverId); - if (!driver.embedded) { - validation.error('plugin_connections_connection_form_host_invalid'); - } - } - - if (!state.config.name?.length) { - validation.error('plugin_connections_connection_form_name_invalid'); - } - - if (state.config.driverId && state.config.configurationType) { - const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']); - - if (!driver.configurationTypes.includes(state.config.configurationType)) { - validation.error('plugin_connections_connection_form_host_configuration_invalid'); - } - } - - if (state.projectId !== null && state.mode === 'create') { - const project = this.projectInfoResource.get(state.projectId); - - if (!project?.canEditDataSources) { - validation.error('plugin_connections_connection_form_project_invalid'); - } - } - - // if (state.config.folder && !state.config.folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) { - // validation.error('connections_connection_folder_validation'); - // } - } - - private async setDefaults(state: IConnectionFormState) { - if (!state.config.driverId) { - return; - } - - const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']); - - state.config.authModelId = driver?.defaultAuthModel; - state.config.configurationType = getDefaultConfigurationType(driver); - - if (!state.config.host) { - state.config.host = driver?.defaultServer || 'localhost'; - } - - if (!state.config.port) { - state.config.port = driver?.defaultPort; - } - - state.config.databaseName = driver?.defaultDatabase; - state.config.url = driver?.sampleURL; - - if (isJDBCConnection(driver)) { - state.config.name = state.config.url; - } else { - state.config.name = getConnectionName(driver.name || '', state.config.host, state.config.port, driver.defaultPort); - } - } - - private async fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider) { - if (!updated) { - return; - } - - if (!state.config.credentials || updated) { - state.config.credentials = {}; - state.config.saveCredentials = false; - } - - if (!state.config.providerProperties || updated) { - state.config.providerProperties = {}; - } - - if (!state.config.mainPropertyValues || updated) { - state.config.mainPropertyValues = {}; - } - - if (!state.info) { - await this.setDefaults(state); - return; - } - - state.config.connectionId = state.info.id; - state.config.configurationType = state.info.configurationType; - - state.config.name = state.info.name; - state.config.description = state.info.description; - state.config.template = state.info.template; - state.config.driverId = state.info.driverId; - - state.config.host = state.info.mainPropertyValues[MAIN_PROPERTY_HOST_KEY]; - state.config.port = state.info.mainPropertyValues[MAIN_PROPERTY_PORT_KEY]; - state.config.serverName = state.info.mainPropertyValues[MAIN_PROPERTY_SERVER_KEY]; - state.config.databaseName = state.info.mainPropertyValues[MAIN_PROPERTY_DATABASE_KEY]; - - state.config.url = state.info.url; - state.config.folder = state.info.folder; - - state.config.authModelId = state.info.authModel; - state.config.saveCredentials = state.info.credentialsSaved; - state.config.sharedCredentials = state.info.sharedCredentials; - - state.config.keepAliveInterval = state.info.keepAliveInterval; - state.config.autocommit = state.info.autocommit; - state.config.readOnly = state.info.readOnly; - - if (state.info.authProperties) { - for (const property of state.info.authProperties) { - if (!property.features.includes('password')) { - state.config.credentials[property.id!] = property.value; - } - } - } - - if (state.info.providerProperties) { - state.config.providerProperties = { ...state.info.providerProperties }; - } - - if (state.info.mainPropertyValues) { - state.config.mainPropertyValues = { ...state.info.mainPropertyValues }; - } - } - - // TODO make a solution to include all required data - private configure(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const configuration = contexts.getContext(connectionFormConfigureContext); - - configuration.include('includeAuthProperties', 'includeCredentialsSaved', 'customIncludeOptions'); - } - - private async prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const credentialsState = contexts.getContext(connectionCredentialsStateContext); - - if (!state.config.driverId || !state.projectId) { - return; - } - - const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties', 'includeMainProperties']); - const tempConfig = toJS(config); - - if (state.mode === 'edit') { - tempConfig.connectionId = state.config.connectionId; - } - - tempConfig.configurationType = state.config.configurationType; - tempConfig.name = state.config.name?.trim(); - - if (tempConfig.name && state.mode === 'create') { - const connections = await state.resource.load(ConnectionInfoProjectKey(state.projectId)); - const connectionNames = connections.map(connection => connection.name); - - tempConfig.name = getUniqueName(tempConfig.name, connectionNames); - } - - tempConfig.description = state.config.description?.trim(); - tempConfig.template = state.config.template; - tempConfig.driverId = state.config.driverId; - tempConfig.keepAliveInterval = Number(state.config.keepAliveInterval); - tempConfig.autocommit = state.config.autocommit; - tempConfig.readOnly = state.config.readOnly; - - if (!state.config.template && state.config.folder) { - tempConfig.folder = state.config.folder; - } - - if (tempConfig.configurationType === DriverConfigurationType.Url) { - tempConfig.url = state.config.url?.trim(); - } - - tempConfig.mainPropertyValues = toJS(state.config.mainPropertyValues); - - if (tempConfig.configurationType === DriverConfigurationType.Manual && !driver.useCustomPage) { - tempConfig.mainPropertyValues[MAIN_PROPERTY_DATABASE_KEY] = state.config.databaseName?.trim(); - - if (!driver.embedded) { - tempConfig.mainPropertyValues[MAIN_PROPERTY_HOST_KEY] = state.config.host?.trim(); - tempConfig.mainPropertyValues[MAIN_PROPERTY_PORT_KEY] = state.config.port?.trim(); - } - - if (driver.requiresServerName) { - tempConfig.mainPropertyValues[MAIN_PROPERTY_SERVER_KEY] = state.config.serverName?.trim(); - } - } - - if ((state.config.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) { - tempConfig.authModelId = state.config.authModelId || driver.defaultAuthModel; - tempConfig.sharedCredentials = state.config.sharedCredentials; - tempConfig.saveCredentials = state.config.saveCredentials || tempConfig.sharedCredentials; - - const properties = await this.getConnectionAuthModelProperties(tempConfig.authModelId, state.info); - - if (this.isCredentialsChanged(properties, state.config.credentials)) { - tempConfig.credentials = this.prepareDynamicProperties(properties, toJS(state.config.credentials)); - } - - if (!tempConfig.saveCredentials) { - credentialsState.requireAuthModel(tempConfig.authModelId); - } - } - - if (driver.providerProperties.length > 0) { - tempConfig.providerProperties = this.prepareDynamicProperties( - driver.providerProperties, - toJS(state.config.providerProperties), - tempConfig.configurationType, - ); - } - - if (driver.useCustomPage && driver.mainProperties.length > 0) { - tempConfig.mainPropertyValues = this.prepareDynamicProperties( - driver.mainProperties, - tempConfig.mainPropertyValues, - tempConfig.configurationType, - ); - } - - runInAction(() => { - Object.assign(config, tempConfig); - }); - } - - private prepareDynamicProperties( - propertiesInfo: ObjectPropertyInfo[], - properties: Record, - configurationType?: DriverConfigurationType, - ) { - const result: Record = { ...properties }; - - for (const propertyInfo of propertiesInfo) { - if (!propertyInfo.id) { - continue; - } - - const supported = configurationType === undefined || propertyInfo.supportedConfigurationTypes?.some(type => type === configurationType); - - if (!supported) { - delete result[propertyInfo.id]; - } else { - const isDefault = isNotNullDefined(propertyInfo.defaultValue); - if (!(propertyInfo.id in result) && isDefault) { - result[propertyInfo.id] = propertyInfo.defaultValue; - } - } - } - - for (const key of Object.keys(result)) { - if (typeof result[key] === 'string') { - result[key] = result[key]?.trim(); - } - } - - return result; - } - - private async formAuthState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const stateContext = contexts.getContext(formStateContext); - - const driver = await this.dbDriverResource.load(config.driverId!, ['includeProviderProperties', 'includeMainProperties']); - const authModel = await this.databaseAuthModelsResource.load(config.authModelId ?? data.info?.authModel ?? driver.defaultAuthModel); - - const providerId = authModel.requiredAuth ?? data.info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID; - - await this.userInfoResource.load(); - - if (!this.userInfoResource.hasToken(providerId)) { - const provider = await this.authProvidersResource.load(providerId); - const message = this.localizationService.translate('plugin_connections_connection_cloud_auth_required', undefined, { - providerLabel: provider.label, - }); - stateContext.setInfo(message); - stateContext.readonly = data.mode === 'edit'; - } - } - - private async formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - if (!data.info) { - return; - } - - const config = contexts.getContext(connectionConfigContext); - const stateContext = contexts.getContext(formStateContext); - const driver = await this.dbDriverResource.load(data.config.driverId!, ['includeProviderProperties', 'includeMainProperties']); - - if ( - !isValuesEqual(config.name, data.info.name, '') || - !isValuesEqual(config.configurationType, data.info.configurationType, DriverConfigurationType.Manual) || - !isValuesEqual(config.description, data.info.description, '') || - !isValuesEqual(config.template, data.info.template, true) || - !isValuesEqual(config.folder, data.info.folder, undefined) || - !isValuesEqual(config.driverId, data.info.driverId, '') || - (config.url !== undefined && !isValuesEqual(config.url, data.info.url, '')) || - (config.host !== undefined && !isValuesEqual(config.host, data.info.host, '')) || - (config.port !== undefined && !isValuesEqual(config.port, data.info.port, '')) || - (config.serverName !== undefined && !isValuesEqual(config.serverName, data.info.serverName, '')) || - (config.databaseName !== undefined && !isValuesEqual(config.databaseName, data.info.databaseName, '')) || - config.credentials !== undefined || - (config.authModelId !== undefined && !isValuesEqual(config.authModelId, data.info.authModel, '')) || - (config.saveCredentials !== undefined && config.saveCredentials !== data.info.credentialsSaved) || - (config.sharedCredentials !== undefined && config.sharedCredentials !== data.info.sharedCredentials) || - (config.providerProperties !== undefined && - !isObjectPropertyInfoStateEqual(driver.providerProperties, config.providerProperties, data.info.providerProperties)) || - (config.mainPropertyValues !== undefined && - !isObjectPropertyInfoStateEqual(driver.mainProperties, config.mainPropertyValues, data.info.mainPropertyValues)) || - (config.keepAliveInterval !== undefined && !isValuesEqual(config.keepAliveInterval, data.info.keepAliveInterval)) || - (config.autocommit !== undefined && !isValuesEqual(config.autocommit, data.info.autocommit)) - ) { - stateContext.markEdited(); - } - } - - private isCredentialsChanged(authProperties: ObjectPropertyInfo[], credentials: Record) { - for (const property of authProperties) { - const value = credentials[property.id!]; - - if (property.features.includes('password')) { - if (value !== undefined) { - return property.features.includes('file') ? true : !!value; - } - } else if (value !== property.value) { - return true; - } - } - return false; - } - - private async getConnectionAuthModelProperties(authModelId: string, connectionInfo?: DatabaseConnection): Promise { - const authModel = await this.databaseAuthModelsResource.load(authModelId); - - let properties = authModel.properties; - - if (connectionInfo?.authProperties && connectionInfo.authProperties.length > 0) { - properties = connectionInfo.authProperties; - } - - return properties; } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionConfig.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionConfig.ts new file mode 100644 index 0000000000..d28bb228d3 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionConfig.ts @@ -0,0 +1,43 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { DriverConfigurationType } from '@cloudbeaver/core-sdk'; +import { schema } from '@cloudbeaver/core-utils'; +import { CONNECTION_NETWORK_HANDLER_SCHEMA } from './IConnectionNetworkHanler.js'; + +export const CONNECTION_CONFIG_SCHEMA = schema.object({ + authModelId: schema.string().optional(), + autocommit: schema.boolean().optional(), + configurationType: schema.enum([DriverConfigurationType.Manual, DriverConfigurationType.Url]).optional(), + connectionId: schema.string().optional(), + credentials: schema.record(schema.any()).optional(), + dataSourceId: schema.string().optional(), + databaseName: schema.string().optional(), + description: schema.string().optional(), + driverId: schema.string().optional(), + folder: schema.string().optional(), + host: schema.string().optional(), + keepAliveInterval: schema.number().optional(), + mainPropertyValues: schema.record(schema.any()).optional(), + name: schema.string().optional(), + networkHandlersConfig: schema.array(CONNECTION_NETWORK_HANDLER_SCHEMA).optional(), + port: schema.string().optional(), + properties: schema.record(schema.any()).optional(), + providerProperties: schema.record(schema.any()).optional(), + readOnly: schema.boolean().optional(), + saveCredentials: schema.boolean().optional(), + selectedSecretId: schema.string().optional(), + serverName: schema.string().optional(), + sharedCredentials: schema.boolean().optional(), + template: schema.boolean().optional(), + templateId: schema.string().optional(), + url: schema.string().optional(), + userName: schema.string().optional(), + userPassword: schema.string().optional(), +}); + +export type IConnectionConfig = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionFormOptionsState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionFormOptionsState.ts index a8c26ff7ca..1283a2f4fb 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionFormOptionsState.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionFormOptionsState.ts @@ -5,51 +5,9 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { DriverConfigurationType, NetworkHandlerAuthType } from '@cloudbeaver/core-sdk'; import { schema } from '@cloudbeaver/core-utils'; +import { CONNECTION_CONFIG_SCHEMA } from './IConnectionConfig.js'; -// TODO export to separate file -export const CONNECTION_NETWORK_HANDLER_SCHEMA = schema.object({ - id: schema.string(), - authType: schema.enum([NetworkHandlerAuthType.Agent, NetworkHandlerAuthType.Password, NetworkHandlerAuthType.PublicKey]).optional(), - enabled: schema.boolean().optional(), - key: schema.string().optional(), - password: schema.string().optional(), - properties: schema.record(schema.any()).optional(), - savePassword: schema.boolean().optional(), - secureProperties: schema.record(schema.any()).optional(), - userName: schema.string().optional(), -}); - -export const CONNECTION_FORM_OPTIONS_SCHEMA = schema.object({ - authModelId: schema.string().optional(), - autocommit: schema.boolean().optional(), - configurationType: schema.enum([DriverConfigurationType.Manual, DriverConfigurationType.Url]).optional(), - connectionId: schema.string().optional(), - credentials: schema.record(schema.any()).optional(), - dataSourceId: schema.string().optional(), - databaseName: schema.string().optional(), - description: schema.string().optional(), - driverId: schema.string().optional(), - folder: schema.string().optional(), - host: schema.string().optional(), - keepAliveInterval: schema.number().optional(), - mainPropertyValues: schema.record(schema.any()).optional(), - name: schema.string().optional(), - networkHandlersConfig: schema.array(CONNECTION_NETWORK_HANDLER_SCHEMA).optional(), - port: schema.string().optional(), - properties: schema.record(schema.any()).optional(), - providerProperties: schema.record(schema.any()).optional(), - readOnly: schema.boolean().optional(), - saveCredentials: schema.boolean().optional(), - selectedSecretId: schema.string().optional(), - serverName: schema.string().optional(), - sharedCredentials: schema.boolean().optional(), - template: schema.boolean().optional(), - templateId: schema.string().optional(), - url: schema.string().optional(), - userName: schema.string().optional(), - userPassword: schema.string().optional(), -}); +export const CONNECTION_FORM_OPTIONS_SCHEMA = CONNECTION_CONFIG_SCHEMA.extend({}); export type IConnectionFormOptionsState = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionNetworkHanler.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionNetworkHanler.ts new file mode 100644 index 0000000000..e149db0e6e --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/IConnectionNetworkHanler.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 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 { NetworkHandlerAuthType } from '@cloudbeaver/core-sdk'; +import { schema } from '@cloudbeaver/core-utils'; + +export const CONNECTION_NETWORK_HANDLER_SCHEMA = schema.object({ + id: schema.string(), + authType: schema.enum([NetworkHandlerAuthType.Agent, NetworkHandlerAuthType.Password, NetworkHandlerAuthType.PublicKey]).optional(), + enabled: schema.boolean().optional(), + key: schema.string().optional(), + password: schema.string().optional(), + properties: schema.record(schema.any()).optional(), + savePassword: schema.boolean().optional(), + secureProperties: schema.record(schema.any()).optional(), + userName: schema.string().optional(), +}); + +export type INetworkHandlerConfig = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index 7b9d597396..891c86bf16 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -34,8 +34,17 @@ import { useS, useTranslate, useAuthenticationAction, + useAutoLoad, } from '@cloudbeaver/core-blocks'; -import { DatabaseAuthModelsResource, type DBDriver, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections'; +import { + ConnectionInfoOriginResource, + ConnectionInfoResource, + createConnectionParam, + DatabaseAuthModelsResource, + type DBDriver, + DBDriverResource, + isLocalConnection, +} from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { EAdminPermission, ServerConfigResource } from '@cloudbeaver/core-root'; @@ -46,14 +55,14 @@ import { ProjectSelect } from '@cloudbeaver/plugin-projects'; import { ConnectionAuthModelCredentialsForm } from '../ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.js'; import { ConnectionAuthModelSelector } from '../ConnectionAuthModelCredentials/ConnectionAuthModelSelector.js'; -import { ConnectionFormService } from '../ConnectionFormService.js'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; import { CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID } from '../SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.js'; import { AdvancedPropertiesForm } from './AdvancedPropertiesForm.js'; import styles from './Options.module.css'; import { ParametersForm } from './ParametersForm.js'; import { ProviderPropertiesForm } from './ProviderPropertiesForm.js'; import { useOptions } from './useOptions.js'; +import type { ConnectionFormRefactoredProps } from '../ConnectionFormServiceRefactored.js'; +import { getConnectionFormOptionsPart } from './getConnectionFormOptionsPart.js'; const PROFILE_AUTH_MODEL_ID = 'profile'; @@ -77,30 +86,34 @@ const driverConfiguration: IDriverConfiguration[] = [ isVisible: driver => driver.configurationTypes.includes(DriverConfigurationType.Url), }, ]; - -export const Options: TabContainerPanelComponent = observer(function Options({ state }) { +// TODO fix the bug with incorrect selected driverId by default in combobox. also type +export const Options: TabContainerPanelComponent = observer(function Options({ formState }) { const isAdmin = usePermission(EAdminPermission.admin); const serverConfigResource = useResource(Options, ServerConfigResource, undefined); const projectInfoResource = useService(ProjectInfoResource); - const service = useService(ConnectionFormService); const formRef = useRef(null); const translate = useTranslate(); - const { info, originInfo, config, availableDrivers, submittingTask: submittingHandlers, disabled } = state; const style = useS(styles); const tabsState = useContext(TabsContext); - const isSharedProject = projectInfoResource.isProjectShared(state.projectId); + const isSharedProject = projectInfoResource.isProjectShared(formState.state.projectId); + const connectionInfoService = useService(ConnectionInfoResource); + const connectionInfoOriginResource = useService(ConnectionInfoOriginResource); + const info = connectionInfoService.get(createConnectionParam(formState.state.projectId, formState.state.config.connectionId!)); + const originInfo = connectionInfoOriginResource.get(createConnectionParam(formState.state.projectId, formState.state.config.connectionId!)); + const optionsPart = getConnectionFormOptionsPart(formState); + const config = optionsPart.state; //@TODO it's here until the profile implementation in the CloudBeaver - const readonly = state.readonly || info?.authModel === PROFILE_AUTH_MODEL_ID; + const readonly = formState.isDisabled || info?.authModel === PROFILE_AUTH_MODEL_ID; - useFormValidator(submittingHandlers.for(service.formValidationTask), formRef.current); - const optionsHook = useOptions(state); + useFormValidator(formState.validationTask, formRef.current); + const optionsHook = useOptions(optionsPart.state, info, formState.mode); const { credentialsSavingEnabled } = useAdministrationSettings(); const driverMap = useResource( Options, DBDriverResource, - { key: config.driverId || null, includes: ['includeProviderProperties', 'includeMainProperties'] as const }, + { key: formState.state.config.driverId || null, includes: ['includeProviderProperties', 'includeMainProperties'] as const }, { onData: data => { optionsHook.setDefaults(data); @@ -152,7 +165,7 @@ export const Options: TabContainerPanelComponent = observe providerId: authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, }); - const edit = state.mode === 'edit'; + const edit = formState.mode === 'edit'; const originLocal = !info || (originInfo?.origin && isLocalConnection(originInfo.origin)); const drivers = driverMap.resource.enabledDrivers.filter(({ id, driverInstalled }) => { @@ -160,9 +173,16 @@ export const Options: TabContainerPanelComponent = observe return false; } - return availableDrivers.includes(id); + return formState.state.availableDrivers.includes(id); }); + function setProject(projectId: string) { + formState.setState({ + ...formState.state, + projectId, + }); + } + let properties = authModel?.properties; if (info?.authProperties && info.authProperties.length > 0 && config.authModelId === info.authModel) { @@ -176,6 +196,8 @@ export const Options: TabContainerPanelComponent = observe tabsState?.open(CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID); } + useAutoLoad(Options, optionsPart); + return ( @@ -199,7 +221,7 @@ export const Options: TabContainerPanelComponent = observe iconSelector={driver => driver.icon} searchable={drivers.length > 10} readOnly={readonly || edit || drivers.length < 2} - disabled={disabled} + disabled={formState.isDisabled} loading={driverMap.isLoading()} tiny fill @@ -233,7 +255,7 @@ export const Options: TabContainerPanelComponent = observe type="text" name="url" state={config} - readOnly={readonly || disabled} + readOnly={readonly || formState.isDisabled} autoComplete={`section-${config.driverId || 'driver'} section-jdbc`} > {translate('plugin_connections_connection_form_part_main_url_jdbc')} @@ -245,7 +267,7 @@ export const Options: TabContainerPanelComponent = observe ) : ( @@ -253,7 +275,7 @@ export const Options: TabContainerPanelComponent = observe config={config} embedded={driver?.embedded} requiresServerName={driver?.requiresServerName} - disabled={disabled} + disabled={formState.isDisabled} readOnly={readonly} originLocal={originLocal} /> @@ -262,16 +284,16 @@ export const Options: TabContainerPanelComponent = observe - + {translate('connections_connection_name')} {!config.template && ( state.setProject(projectId)} + onChange={setProject} /> )} {!config.template && ( @@ -289,7 +311,7 @@ export const Options: TabContainerPanelComponent = observe )} - @@ -304,7 +326,7 @@ export const Options: TabContainerPanelComponent = observe name="sharedCredentials" title={translate('connections_connection_share_credentials_tooltip')} state={config} - disabled={disabled || readonly} + disabled={formState.isDisabled || readonly} keepSize > {translate('connections_connection_share_credentials')} @@ -315,7 +337,7 @@ export const Options: TabContainerPanelComponent = observe applicableAuthModels={applicableAuthModels} readonlyAuthModelId={!originLocal} readonly={readonly} - disabled={disabled} + disabled={formState.isDisabled} onAuthModelChange={handleAuthModelSelect} /> {!sharedCredentials ? ( @@ -325,7 +347,7 @@ export const Options: TabContainerPanelComponent = observe credentials={config.credentials} properties={properties} readonly={readonly} - disabled={disabled} + disabled={formState.isDisabled} /> )} @@ -342,7 +364,7 @@ export const Options: TabContainerPanelComponent = observe id={config.connectionId + 'authNeeded'} name="saveCredentials" state={config} - disabled={disabled || readonly || config.sharedCredentials} + disabled={formState.isDisabled || readonly || config.sharedCredentials} mod={['primary']} title={translate( !isSharedProject || serverConfigResource.data?.distributed @@ -361,10 +383,10 @@ export const Options: TabContainerPanelComponent = observe )} {driver?.providerProperties && ( - + )} - + diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/useOptions.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/useOptions.ts index 71838be53e..58c111c032 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/useOptions.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/useOptions.ts @@ -10,13 +10,12 @@ import { runInAction } from 'mobx'; import { useObjectRef } from '@cloudbeaver/core-blocks'; import { type DBDriver, DBDriverResource, isJDBCConnection } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; -import type { DatabaseAuthModel } from '@cloudbeaver/core-sdk'; - -import type { IConnectionFormState } from '../IConnectionFormProps.js'; +import type { ConnectionConfig, DatabaseAuthModel, DatabaseConnectionFragment } from '@cloudbeaver/core-sdk'; import { getConnectionName } from './getConnectionName.js'; import { getDefaultConfigurationType } from './getDefaultConfigurationType.js'; +import type { FormMode } from '@cloudbeaver/core-ui'; -export function useOptions(state: IConnectionFormState) { +export function useOptions(config: ConnectionConfig, info: DatabaseConnectionFragment | undefined, mode: FormMode) { const dbDriverResource = useService(DBDriverResource); const refObject = useObjectRef( () => ({ @@ -24,16 +23,16 @@ export function useOptions(state: IConnectionFormState) { prevDriverId: null as string | null, }), { - state, + config, + info, + mode, }, ); return useObjectRef({ updateNameTemplate(driver: DBDriver | undefined) { runInAction(() => { - const { - state: { config, info }, - } = refObject; + const { config, info } = refObject; if (isJDBCConnection(driver, info)) { refObject.prevName = config.url || ''; @@ -54,10 +53,7 @@ export function useOptions(state: IConnectionFormState) { }, setDefaults(driver: DBDriver | undefined) { runInAction(() => { - const { - state: { config, info }, - prevDriverId, - } = refObject; + const { config, info, prevDriverId } = refObject; if (info || driver?.id !== config.driverId) { return; @@ -72,7 +68,7 @@ export function useOptions(state: IConnectionFormState) { refObject.prevDriverId = driver?.id || null; if (!config.configurationType || !driver?.configurationTypes.includes(config.configurationType)) { - state.config.configurationType = getDefaultConfigurationType(driver); + config.configurationType = getDefaultConfigurationType(driver); } if ((!prevDriver && config.host === undefined) || config.host === prevDriver?.defaultServer) { @@ -103,9 +99,7 @@ export function useOptions(state: IConnectionFormState) { }); }, setAuthModel(model: DatabaseAuthModel) { - const { - state: { config, info }, - } = refObject; + const { config, info } = refObject; config.credentials = {}; @@ -119,14 +113,12 @@ export function useOptions(state: IConnectionFormState) { } } - refObject.state.checkFormState(); + // TODO trigger form reload or Options part reload + // refObject.state.checkFormState(); }, isNameAutoFill() { - const { - prevName, - state: { config, mode }, - } = refObject; + const { prevName, config, mode } = refObject; const isAutoFill = config.name === prevName || prevName === null; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx index 777afd2e16..af7aa1aac1 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx @@ -9,24 +9,29 @@ import { observer } from 'mobx-react-lite'; import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; import { Button, getComputed, type PlaceholderComponent, useResource, useTranslate, useAuthenticationAction } from '@cloudbeaver/core-blocks'; -import { DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections'; +import { ConnectionInfoResource, createConnectionParam, DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections'; +import type { ConnectionFormRefactoredProps } from '../ConnectionFormServiceRefactored.js'; +import { useService } from '@cloudbeaver/core-di'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; - -export const AuthenticationButton: PlaceholderComponent = observer(function ConnectionFormAuthenticationAction({ state }) { +export const AuthenticationButton: PlaceholderComponent = observer(function ConnectionFormAuthenticationAction({ + formState, +}) { const translate = useTranslate(); - const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, state.config.driverId || null); + const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, formState.state.config.driverId || null); + const connectionInfoService = useService(ConnectionInfoResource); + const info = connectionInfoService.get(createConnectionParam(formState.state.projectId, formState.state.config.connectionId!)); const driver = driverMap.data; const { data: authModel } = useResource( ConnectionFormAuthenticationAction, DatabaseAuthModelsResource, - getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null), + getComputed(() => formState.state.config.authModelId || info?.authModel || driver?.defaultAuthModel || null), ); const authentication = useAuthenticationAction({ - providerId: authModel?.requiredAuth ?? state.info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, - onAuthenticate: () => state.loadConnectionInfo(), + providerId: authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, + // TODO should we remove it? + // onAuthenticate: () => state.loadConnectionInfo(), }); if (authentication.authorized) { @@ -34,27 +39,29 @@ export const AuthenticationButton: PlaceholderComponent = } return ( - ); }); -export const ConnectionFormAuthenticationAction: PlaceholderComponent = observer(function ConnectionFormAuthenticationAction({ - state, -}) { - const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, state.config.driverId || null); +export const ConnectionFormAuthenticationAction: PlaceholderComponent = observer( + function ConnectionFormAuthenticationAction({ formState }) { + const driverMap = useResource(ConnectionFormAuthenticationAction, DBDriverResource, formState.state.config.driverId || null); + const connectionInfoService = useService(ConnectionInfoResource); + const info = connectionInfoService.get(createConnectionParam(formState.state.projectId, formState.state.config.connectionId!)); - const driver = driverMap.data; - const { data: authModel } = useResource( - ConnectionFormAuthenticationAction, - DatabaseAuthModelsResource, - getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null), - ); + const driver = driverMap.data; + const { data: authModel } = useResource( + ConnectionFormAuthenticationAction, + DatabaseAuthModelsResource, + getComputed(() => formState.state.config.authModelId || info?.authModel || driver?.defaultAuthModel || null), + ); - if (!authModel?.requiredAuth && !state.info?.requiredAuth) { - return null; - } + if (!authModel?.requiredAuth && !info?.requiredAuth) { + return null; + } - return ; -}); + return ; + }, +); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts index d13752d4e5..1c2ef01a8e 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionOriginInfoTabService.ts @@ -5,69 +5,50 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import React from 'react'; -import { isLocalConnection } from '@cloudbeaver/core-connections'; +import { ConnectionInfoOriginResource, createConnectionParam, isLocalConnection } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { ConnectionFormService } from '../ConnectionFormService.js'; +import { ConnectionFormServiceRefactored } from '../ConnectionFormServiceRefactored.js'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; -export const ConnectionFormAuthenticationAction = React.lazy(async () => { - const { ConnectionFormAuthenticationAction } = await import('./ConnectionFormAuthenticationAction.js'); - return { default: ConnectionFormAuthenticationAction }; -}); -export const OriginInfo = React.lazy(async () => { - const { OriginInfo } = await import('./OriginInfo.js'); - return { default: OriginInfo }; -}); -export const OriginInfoTab = React.lazy(async () => { - const { OriginInfoTab } = await import('./OriginInfoTab.js'); - return { default: OriginInfoTab }; -}); +export const ConnectionFormAuthenticationAction = importLazyComponent(() => + import('./ConnectionFormAuthenticationAction.js').then(m => m.ConnectionFormAuthenticationAction), +); + +export const OriginInfo = importLazyComponent(() => import('./OriginInfo.js').then(m => m.OriginInfo)); +export const OriginInfoTab = importLazyComponent(() => import('./OriginInfoTab.js').then(m => m.OriginInfoTab)); @injectable() export class ConnectionOriginInfoTabService extends Bootstrap { constructor( - private readonly connectionFormService: ConnectionFormService, - // private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, - // private readonly connectionInfoOriginResource: ConnectionInfoOriginResource, + private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, + private readonly connectionInfoOriginResource: ConnectionInfoOriginResource, ) { super(); } override register(): void { - this.connectionFormService.tabsContainer.add({ + this.connectionFormServiceRefactored.parts.add({ key: 'origin', order: 3, tab: () => OriginInfoTab, panel: () => OriginInfo, - stateGetter: () => () => ({}), - isHidden: (tabId, props) => (props?.state.originInfo ? isLocalConnection(props.state.originInfo.origin) : true), + isHidden: (tabId, props) => { + const projectId = props?.formState.state.projectId; + const connectionId = props?.formState.state.config.connectionId; + + if (!projectId || !connectionId) { + return true; + } + + // TODO it is better to preload it + const originInfo = this.connectionInfoOriginResource.get(createConnectionParam(projectId, connectionId)); + + return originInfo ? isLocalConnection(originInfo.origin) : true; + }, }); - // TODO uncomment once refactored - // this.connectionFormServiceRefactored.parts.add({ - // key: 'origin', - // order: 3, - // tab: () => OriginInfoTab, - // panel: () => OriginInfo, - // isHidden: (tabId, props) => { - // const projectId = props?.formState.state.connectionInfoParams?.projectId; - // const connectionId = props?.formState.state.connectionInfoParams?.connectionId; - - // if (!projectId || !connectionId) { - // return true; - // } - - // const originInfo = this.connectionInfoOriginResource.get(createConnectionParam(projectId, connectionId)); - - // return originInfo ? isLocalConnection(originInfo.origin) : true; - // }, - // }); - - // TODO uncomment once refactored - // this.connectionFormServiceRefactored.actionsContainer.add(ConnectionFormAuthenticationAction, 0); - - this.connectionFormService.actionsContainer.add(ConnectionFormAuthenticationAction, 0); + this.connectionFormServiceRefactored.actionsContainer.add(ConnectionFormAuthenticationAction, 0); } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx index faa3a65aee..3ca394e2a5 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx @@ -23,29 +23,31 @@ import { } from '@cloudbeaver/core-blocks'; import { ConnectionInfoOriginDetailsResource, + ConnectionInfoOriginResource, + ConnectionInfoResource, createConnectionParam, DatabaseAuthModelsResource, DBDriverResource, } from '@cloudbeaver/core-connections'; import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; import styles from './OriginInfo.module.css'; +import type { ConnectionFormRefactoredProps } from '../ConnectionFormServiceRefactored.js'; +import { useService } from '@cloudbeaver/core-di'; -export const OriginInfo: TabContainerPanelComponent = observer(function OriginInfo({ - tabId, - state: { info, resource, config }, -}) { +export const OriginInfo: TabContainerPanelComponent = observer(function OriginInfo({ tabId, formState }) { const tab = useTab(tabId); const translate = useTranslate(); const userInfoLoader = useResource(OriginInfo, UserInfoResource, undefined); const state = useTabState>(); const style = useS(styles); - const driverLoader = useResource(OriginInfo, DBDriverResource, config.driverId ?? null); + const connectionInfoService = useService(ConnectionInfoResource); + const info = connectionInfoService.get(createConnectionParam(formState.state.projectId, formState.state.config.connectionId!)); + const driverLoader = useResource(OriginInfo, DBDriverResource, formState.state.config.driverId ?? null); const authModeLoader = useResource( OriginInfo, DatabaseAuthModelsResource, - config.authModelId ?? info?.authModel ?? driverLoader.data?.defaultAuthModel ?? null, + formState.state.config.authModelId ?? info?.authModel ?? driverLoader.data?.defaultAuthModel ?? null, ); const providerId = authModeLoader.data?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID; @@ -56,7 +58,7 @@ export const OriginInfo: TabContainerPanelComponent = obse const connectionOriginDetailsResource = useResource(OriginInfo, ConnectionInfoOriginDetailsResource, connectionId, { active: isAuthenticated, }); - const connection = useResource(OriginInfo, resource, connectionId, { + const connection = useResource(OriginInfo, ConnectionInfoOriginResource, connectionId, { active: isAuthenticated, onData: connection => { runInAction(() => { diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx index aa82dbbf20..6d3c50a423 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfoTab.tsx @@ -7,16 +7,23 @@ */ import { observer } from 'mobx-react-lite'; -import { Translate } from '@cloudbeaver/core-blocks'; +import { Translate, useResource } from '@cloudbeaver/core-blocks'; import { Tab, type TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; +import type { ConnectionFormRefactoredProps } from '../ConnectionFormServiceRefactored.js'; +import { ConnectionInfoOriginResource, createConnectionParam } from '@cloudbeaver/core-connections'; + +export const OriginInfoTab: TabContainerTabComponent = observer(function OriginInfoTab({ formState, ...rest }) { + const originInfo = useResource( + OriginInfoTab, + ConnectionInfoOriginResource, + createConnectionParam(formState.state.projectId, formState.state.config.connectionId!), + ); -export const OriginInfoTab: TabContainerTabComponent = observer(function OriginInfoTab({ state: { originInfo }, ...rest }) { return ( - + ); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionFormSSHPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionFormSSHPart.ts index c2630e4144..0a9bc5d37c 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionFormSSHPart.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionFormSSHPart.ts @@ -9,11 +9,11 @@ import { FormPart, formValidationContext, type IFormState } from '@cloudbeaver/c import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { DriverConfigurationType, NetworkHandlerAuthType, type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; -import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; import { ConnectionInfoResource, createConnectionParam, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; import { toJS } from 'mobx'; import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext.js'; import type { IConnectionFormStateRefactored } from '../IConnectionFormStateRefactored.js'; +import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js'; export class ConnectionFormSSHPart extends FormPart { constructor( @@ -24,20 +24,16 @@ export class ConnectionFormSSHPart extends FormPart { - if (!this.isChanged) { - return; - } // TODO should we load instead of get? - const info = this.connectionInfoResource.get(createConnectionParam(this.formState.state.projectId, this.formState.state.connectionId)); + const info = this.connectionInfoResource.get(createConnectionParam(this.formState.state.projectId, this.formState.state.config.connectionId!)); const initialConfig = info?.networkHandlersConfig?.find(handler => handler.id === SSH_TUNNEL_ID); - const optionsPart = getConnectionFormOptionsPart(this.formState); - if (!optionsPart.state.networkHandlersConfig) { - optionsPart.state.networkHandlersConfig = []; + if (!this.formState.state.config.networkHandlersConfig) { + this.formState.state.config.networkHandlersConfig = []; } - if (!optionsPart.state.networkHandlersConfig.some(state => state.id === SSH_TUNNEL_ID)) { - optionsPart.state.networkHandlersConfig.push({ + if (!this.formState.state.config.networkHandlersConfig.some(state => state.id === SSH_TUNNEL_ID)) { + this.formState.state.config.networkHandlersConfig.push({ id: SSH_TUNNEL_ID, enabled: false, authType: NetworkHandlerAuthType.Password, @@ -57,6 +53,15 @@ export class ConnectionFormSSHPart extends FormPart, + contexts: IExecutionContextProvider>, + ): void | Promise { + const configuration = contexts.getContext(connectionFormConfigureContext); + + configuration.include('includeNetworkHandlersConfig'); + } + protected override saveChanges( data: IFormState, contexts: IExecutionContextProvider>, @@ -68,11 +73,10 @@ export class ConnectionFormSSHPart extends FormPart, contexts: IExecutionContextProvider>, ): void | Promise { - const optionsPart = getConnectionFormOptionsPart(this.formState); - const config = optionsPart.state; + const config = this.formState.state.config; const credentialsState = contexts.getContext(connectionCredentialsStateContext); const urlType = config.configurationType === DriverConfigurationType.Url; - const info = this.connectionInfoResource.get(createConnectionParam(this.formState.state.projectId, this.formState.state.connectionId)); + const info = this.connectionInfoResource.get(createConnectionParam(this.formState.state.projectId, this.formState.state.config.connectionId!)); if (urlType || !config.networkHandlersConfig || config.networkHandlersConfig.length === 0) { return; @@ -112,6 +116,7 @@ export class ConnectionFormSSHPart extends FormPart>, ): void | Promise { const validation = contexts.getContext(formValidationContext); - const optionsPart = getConnectionFormOptionsPart(this.formState); - const info = this.connectionInfoResource.get(createConnectionParam(this.formState.state.projectId, this.formState.state.connectionId)); + const info = this.connectionInfoResource.get(createConnectionParam(this.formState.state.projectId, this.formState.state.config.connectionId!)); - if (!optionsPart.state.networkHandlersConfig) { + if (!this.formState.state.config.networkHandlersConfig) { return; } - const handler = optionsPart.state.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID); + const handler = this.formState.state.config.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID); if (!handler) { return; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts index ac4f6db32f..41f8e46a53 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts @@ -5,20 +5,14 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, makeObservable, toJS } from 'mobx'; import React from 'react'; import { DBDriverResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { DriverConfigurationType, NetworkHandlerAuthType, type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; -import { formStateContext } from '@cloudbeaver/core-ui'; +import { DriverConfigurationType } from '@cloudbeaver/core-sdk'; -import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js'; -import { ConnectionFormService } from '../ConnectionFormService.js'; -import { connectionConfigContext } from '../Contexts/connectionConfigContext.js'; -import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext.js'; -import type { IConnectionFormFillConfigData, IConnectionFormState, IConnectionFormSubmitData } from '../IConnectionFormProps.js'; +import { ConnectionFormServiceRefactored } from '../ConnectionFormServiceRefactored.js'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; export const SSHTab = React.lazy(async () => { const { SSHTab } = await import('./SSHTab.js'); @@ -32,29 +26,24 @@ export const SSHPanel = React.lazy(async () => { @injectable() export class ConnectionSSHTabService extends Bootstrap { constructor( - private readonly connectionFormService: ConnectionFormService, private readonly dbDriverResource: DBDriverResource, - // private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, + private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, ) { super(); - - makeObservable(this, { - fillConfig: action, - prepareConfig: action, - }); } override register(): void { - this.connectionFormService.tabsContainer.add({ + this.connectionFormServiceRefactored.parts.add({ key: 'ssh', name: 'plugin_connections_connection_form_part_main', order: 3, tab: () => SSHTab, panel: () => SSHPanel, isHidden: (tabId, props) => { - if (props?.state.config.driverId) { - const driver = this.dbDriverResource.get(props.state.config.driverId); - const urlType = props.state.config.configurationType === DriverConfigurationType.Url; + if (props?.formState.state.config.driverId) { + const driver = this.dbDriverResource.get(props?.formState.state.config.driverId); + const optionsPart = getConnectionFormOptionsPart(props.formState); + const urlType = optionsPart.state.configurationType === DriverConfigurationType.Url; return urlType || !driver?.applicableNetworkHandlers.includes(SSH_TUNNEL_ID); } @@ -62,238 +51,5 @@ export class ConnectionSSHTabService extends Bootstrap { return true; }, }); - - // TODO uncomment it once refactored - // this.connectionFormServiceRefactored.parts.add({ - // key: 'ssh', - // name: 'plugin_connections_connection_form_part_main', - // order: 3, - // tab: () => SSHTab, - // panel: () => SSHPanel, - // isHidden: (tabId, props) => { - // if (!props?.formState) { - // return true; - // } - // const optionsPart = getConnectionFormOptionsPart(props.formState); - // const driverId = props.formState.state.driverId; - - // if (driverId) { - // const driver = driverId ? this.dbDriverResource.get(driverId) : undefined; - // const urlType = optionsPart.state.configurationType === DriverConfigurationType.Url; - - // return urlType || !driver?.applicableNetworkHandlers.includes(SSH_TUNNEL_ID); - // } - - // return true; - // }, - // }); - - this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.connectionFormService.formValidationTask.addHandler(this.validate.bind(this)); - - this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)); - - this.connectionFormService.configureTask.addHandler(this.configure.bind(this)); - - this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - } - - private fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider) { - if (!updated) { - return; - } - const initialConfig = state.info?.networkHandlersConfig?.find(handler => handler.id === SSH_TUNNEL_ID); - - if (!state.config.networkHandlersConfig) { - state.config.networkHandlersConfig = []; - } - - if (!state.config.networkHandlersConfig.some(state => state.id === SSH_TUNNEL_ID)) { - state.config.networkHandlersConfig.push({ - id: SSH_TUNNEL_ID, - enabled: false, - authType: NetworkHandlerAuthType.Password, - password: '', - savePassword: false, - userName: '', - key: '', - ...initialConfig, - properties: { - port: 22, - host: '', - aliveInterval: '0', - sshConnectTimeout: '10000', - ...initialConfig?.properties, - }, - }); - } - } - - // TODO make a solution to include all required data - private configure(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const configuration = contexts.getContext(connectionFormConfigureContext); - - configuration.include('includeNetworkHandlersConfig'); - } - - private validate({ state: { config, info } }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const validation = contexts.getContext(this.connectionFormService.connectionValidationContext); - - if (!config.networkHandlersConfig) { - return; - } - - const handler = config.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID); - - if (!handler) { - return; - } - - if (handler.enabled) { - const initial = info?.networkHandlersConfig?.find(h => h.id === handler.id); - if (this.isChanged(handler, initial)) { - if (handler.savePassword && !handler.userName?.length) { - validation.error("Field SSH 'User' can't be empty"); - } - - if (!handler.properties?.host?.length) { - validation.error("Field SSH 'Host' can't be empty"); - } - - const port = Number(handler.properties?.port); - if (Number.isNaN(port) || port < 1) { - validation.error("Field SSH 'Port' can't be empty"); - } - } - - const keyAuth = handler.authType === NetworkHandlerAuthType.PublicKey; - const keySaved = initial?.key === ''; - if (keyAuth && handler.savePassword && !keySaved && !handler.key?.length) { - validation.error("Field SSH 'Private key' can't be empty"); - } - - const passwordSaved = initial?.password === '' && initial.authType === handler.authType; - if (!keyAuth && handler.savePassword && !passwordSaved && !handler.password?.length) { - validation.error("Field SSH 'Password' can't be empty"); - } - } - } - - private prepareConfig({ state }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const credentialsState = contexts.getContext(connectionCredentialsStateContext); - const urlType = state.config.configurationType === DriverConfigurationType.Url; - - if (urlType || !state.config.networkHandlersConfig || state.config.networkHandlersConfig.length === 0) { - return; - } - - let handlerConfig: NetworkHandlerConfigInput | undefined; - - const handler = state.config.networkHandlersConfig.find(handler => handler.id === SSH_TUNNEL_ID); - - if (!handler) { - return; - } - - const initial = state.info?.networkHandlersConfig?.find(h => h.id === handler.id); - const passwordChanged = this.isPasswordChanged(handler, initial); - const keyChanged = this.isKeyChanged(handler, initial); - - if (this.isChanged(handler, initial) || passwordChanged || keyChanged) { - handlerConfig = { - ...handler, - savePassword: handler.savePassword || config.sharedCredentials, - key: handler.authType === NetworkHandlerAuthType.PublicKey && keyChanged ? handler.key : undefined, - password: passwordChanged ? handler.password : undefined, - }; - - delete handlerConfig.secureProperties; - } - - if (handler.enabled && !handler.savePassword) { - credentialsState.requireNetworkHandler(handler.id); - } - - if (handlerConfig) { - if (!config.networkHandlersConfig) { - config.networkHandlersConfig = []; - } - - handlerConfig = this.getTrimmedSSHConfig(handlerConfig); - config.networkHandlersConfig.push(handlerConfig); - } - } - - private getTrimmedSSHConfig(input: NetworkHandlerConfigInput): NetworkHandlerConfigInput { - const trimmedInput = toJS(input); - const attributesToTrim = Object.keys(input) as (keyof NetworkHandlerConfigInput)[]; - - for (const key of attributesToTrim) { - if (typeof trimmedInput[key] === 'string') { - trimmedInput[key] = trimmedInput[key]?.trim(); - } - } - - for (const key in trimmedInput.properties) { - if (typeof trimmedInput.properties[key] === 'string') { - trimmedInput.properties[key] = trimmedInput.properties[key]?.trim(); - } - } - - return trimmedInput; - } - - private formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - if (config.networkHandlersConfig !== undefined) { - const stateContext = contexts.getContext(formStateContext); - - stateContext.markEdited(); - } - } - - private isChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { - if (!initial && !handler.enabled) { - return false; - } - - const port = Number(initial?.properties?.port); - const formPort = Number(handler.properties?.port); - - if ( - handler.enabled !== initial?.enabled || - handler.authType !== initial?.authType || - handler.savePassword !== initial?.savePassword || - handler.userName !== initial?.userName || - handler.properties?.host !== initial?.properties?.host || - port !== formPort || - handler.properties?.aliveInterval !== initial?.properties?.aliveInterval || - handler.properties?.sshConnectTimeout !== initial?.properties?.sshConnectTimeout - ) { - return true; - } - - return false; - } - - private isPasswordChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { - if (!initial && !handler.enabled) { - return false; - } - - return ( - (((initial?.password === null && handler.password !== null) || initial?.password === '') && handler.password !== '') || - !!handler.password?.length - ); - } - - private isKeyChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { - if (!initial && !handler.enabled) { - return false; - } - - return (((initial?.key === null && handler.key !== null) || initial?.key === '') && handler.key !== '') || !!handler.key?.length; } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/IConnectionFromSSHState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/IConnectionFromSSHState.ts index 578b4ad00d..4ffdd6b7cc 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/IConnectionFromSSHState.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/IConnectionFromSSHState.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { schema } from '@cloudbeaver/core-utils'; -import { CONNECTION_NETWORK_HANDLER_SCHEMA } from '../Options/IConnectionFormOptionsState.js'; +import { CONNECTION_NETWORK_HANDLER_SCHEMA } from '../Options/IConnectionNetworkHanler.js'; export const CONNECTION_FORM_SSH_SCHEMA = schema.object({}).extend(CONNECTION_NETWORK_HANDLER_SCHEMA.shape); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx index 93ca791def..a220d1e654 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx @@ -22,32 +22,36 @@ import { s, Switch, useAdministrationSettings, + useAutoLoad, useResource, useS, useTranslate, } from '@cloudbeaver/core-blocks'; -import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; +import { ConnectionInfoResource, createConnectionParam, NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import { NetworkHandlerAuthType, type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; -import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; +import type { IFormState, TabContainerPanelComponent } from '@cloudbeaver/core-ui'; import { isSafari } from '@cloudbeaver/core-utils'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; import { authTypes } from './authTypes.js'; import styles from './SSH.module.css'; import { SSHKeyUploader } from './SSHKeyUploader.js'; +import type { IConnectionFormStateRefactored } from '../IConnectionFormStateRefactored.js'; +import { getConnectionFormSSHPart } from './getConnectionFormSSHPart.js'; -interface Props extends IConnectionFormProps { +interface Props { handlerState: NetworkHandlerConfigInput; + formState: IFormState; } -export const SSH: TabContainerPanelComponent = observer(function SSH({ state: formState, handlerState }) { - const { info, readonly, disabled: formDisabled } = formState; +export const SSH: TabContainerPanelComponent = observer(function SSH({ formState, handlerState }) { const [loading, setLoading] = useState(false); const { credentialsSavingEnabled } = useAdministrationSettings(); const serverConfigResource = useResource(SSH, ServerConfigResource, undefined); + const connectionInfoService = useService(ConnectionInfoResource); + const info = connectionInfoService.get(createConnectionParam(formState.state.projectId, formState.state.config.connectionId!)); const initialConfig = info?.networkHandlersConfig?.find(handler => handler.id === SSH_TUNNEL_ID); @@ -71,7 +75,7 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st const style = useS(styles); const translate = useTranslate(); - const disabled = formDisabled || loading; + const disabled = formState.isDisabled || loading; const enabled = handlerState.enabled || false; const keyAuth = handlerState.authType === NetworkHandlerAuthType.PublicKey; const passwordFilled = (initialConfig?.password === null && handlerState.password !== '') || !!handlerState.password?.length; @@ -80,7 +84,8 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st const passwordSaved = initialConfig?.password === '' && initialConfig.authType === handlerState.authType; const keySaved = initialConfig?.key === ''; const projectInfoResource = useService(ProjectInfoResource); - const isSharedProject = projectInfoResource.isProjectShared(formState.projectId); + const isSharedProject = projectInfoResource.isProjectShared(formState.state.projectId); + const SSHPart = getConnectionFormSSHPart(formState); const aliveIntervalLabel = translate('connections_network_handler_ssh_tunnel_advanced_settings_alive_interval'); const connectTimeoutLabel = translate('connections_network_handler_ssh_tunnel_advanced_settings_connect_timeout'); @@ -89,11 +94,13 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st handlerState.password = ''; }, []); + useAutoLoad(SSH, [SSHPart]); + return (
- + {translate('connections_network_handler_ssh_tunnel_enable')} = observer(function SSH({ st items={authTypes} keySelector={value => value.key} valueSelector={value => value.label} - disabled={disabled || readonly || !enabled} + disabled={disabled || formState.isDisabled || !enabled} tiny onSelect={authTypeChangeHandler} > {translate('connections_network_handler_ssh_tunnel_auth_type')} - + {translate('connections_network_handler_ssh_tunnel_host')} - + {translate('connections_network_handler_ssh_tunnel_port')} @@ -121,7 +142,7 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st type="text" name="userName" state={handlerState} - readOnly={readonly || disabled || !enabled} + readOnly={formState.isDisabled || disabled || !enabled} required={handlerState.savePassword} tiny fill @@ -133,7 +154,7 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st name="password" autoComplete={isSafari ? 'section-connection-ssh-authentication section-ssh password' : 'new-password'} state={handlerState} - readOnly={readonly || disabled || !enabled} + readOnly={formState.isDisabled || disabled || !enabled} required={!passwordSaved && !keyAuth && handlerState.savePassword} description={passwordSaved ? translate('ui_processing_saved') : undefined} tiny @@ -141,9 +162,9 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st > {passwordLabel} - {keyAuth && } + {keyAuth && } - {credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && ( + {credentialsSavingEnabled && !formState.state.config.template && !formState.state.config.sharedCredentials && ( = observer(function SSH({ st )} name="savePassword" state={handlerState} - disabled={disabled || !enabled || readonly} + disabled={disabled || !enabled || formState.isDisabled} > {translate( !isSharedProject || serverConfigResource.data?.distributed @@ -169,7 +190,7 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st type="number" name="aliveInterval" state={handlerState.properties} - readOnly={readonly || disabled || !enabled} + readOnly={formState.isDisabled || disabled || !enabled} labelTooltip={aliveIntervalLabel} tiny > @@ -179,7 +200,7 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st type="number" name="sshConnectTimeout" state={handlerState.properties} - readOnly={readonly || disabled || !enabled} + readOnly={formState.isDisabled || disabled || !enabled} labelTooltip={connectTimeoutLabel} tiny > diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHPanel.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHPanel.tsx index e7937036ce..3cf06a02b2 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHPanel.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHPanel.tsx @@ -10,11 +10,16 @@ import { observer } from 'mobx-react-lite'; import { SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; import { SSH } from './SSH.js'; +import type { ConnectionFormRefactoredProps } from '../ConnectionFormServiceRefactored.js'; +import { useAutoLoad } from '@cloudbeaver/core-blocks'; +import { getConnectionFormSSHPart } from './getConnectionFormSSHPart.js'; -export const SSHPanel: TabContainerPanelComponent = observer(function SSHPanel(props) { - const state = props.state.config.networkHandlersConfig?.find(state => state.id === SSH_TUNNEL_ID); +export const SSHPanel: TabContainerPanelComponent = observer(function SSHPanel(props) { + const SSHPart = getConnectionFormSSHPart(props.formState); + const state = props.formState.state.config.networkHandlersConfig?.find(state => state.id === SSH_TUNNEL_ID); + + useAutoLoad(SSHPanel, SSHPart); if (!state) { return null; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx index 92c7b13aa0..f9fa446103 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx @@ -11,9 +11,9 @@ import { Translate, useResource } from '@cloudbeaver/core-blocks'; import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; import { Tab, type TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; +import type { ConnectionFormRefactoredProps } from '../ConnectionFormServiceRefactored.js'; -export const SSHTab: TabContainerTabComponent = observer(function SSHTab(props) { +export const SSHTab: TabContainerTabComponent = observer(function SSHTab(props) { const handler = useResource(SSHTab, NetworkHandlerResource, SSH_TUNNEL_ID); return ( diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts index 4e907bbda1..d9a109d560 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts @@ -19,13 +19,14 @@ import { type NetworkHandlerResource, } from '@cloudbeaver/core-connections'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; import { getSSLDefaultConfig } from './getSSLDefaultConfig.js'; import { toJS } from 'mobx'; import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext.js'; import { PROPERTY_FEATURE_SECURED } from './PROPERTY_FEATURE_SECURED.js'; import { SSL_CODE_NAME } from './SSL_CODE_NAME.js'; +import { connectionFormConfigureContext } from '../connectionFormConfigureContext.js'; +// TODO should I have networkHandler state here? export class ConnectionFormSSLPart extends FormPart { constructor( formState: IFormState, @@ -37,18 +38,16 @@ export class ConnectionFormSSLPart extends FormPart { - if (!this.isChanged || !this.formState.state.driverId) { + if (!this.isChanged || !this.formState.state.config.driverId) { return; } - const driver = await this.dbDriverResource.load(this.formState.state.driverId); + const driver = await this.dbDriverResource.load(this.formState.state.config.driverId); const handlers = await this.networkHandlerResource.load(CachedMapAllKey); - - const optionsPart = getConnectionFormOptionsPart(this.formState); const handler = getSSLDriverHandler(handlers, driver?.applicableNetworkHandlers ?? []); const info = this.connectionInfoResource.get( createConnectionParam({ - id: this.formState.state.connectionId, + id: this.formState.state.config.connectionId!, projectId: this.formState.state.projectId, }), ); @@ -59,31 +58,39 @@ export class ConnectionFormSSLPart extends FormPart h.id === handler.id); - if (!optionsPart?.state.networkHandlersConfig) { - optionsPart.state.networkHandlersConfig = []; + if (!this.formState.state.config.networkHandlersConfig) { + this.formState.state.config.networkHandlersConfig = []; } - if (!optionsPart?.state.networkHandlersConfig.some(state => state.id === handler.id)) { + if (!this.formState.state.config.networkHandlersConfig.some(state => state.id === handler.id)) { const config: NetworkHandlerConfigInput = initialConfig ? toJS(initialConfig) : getSSLDefaultConfig(handler.id); if (config.secureProperties) { config.properties = { ...config.properties, ...config.secureProperties }; } - optionsPart?.state.networkHandlersConfig.push(config); + this.formState.state.config.networkHandlersConfig.push(config); } } + protected override configure( + data: IFormState, + contexts: IExecutionContextProvider>, + ): void | Promise { + const configuration = contexts.getContext(connectionFormConfigureContext); + + configuration.include('includeNetworkHandlersConfig'); + } + protected override async format( - data: IFormState<{ projectId: string; connectionId: string; driverId: string; submitType: 'submit' | 'test' }>, - contexts: IExecutionContextProvider>, + data: IFormState, + contexts: IExecutionContextProvider>, ): Promise { - const optionsPart = getConnectionFormOptionsPart(this.formState); - const config = optionsPart.state; + const config = this.formState.state.config; const credentialsState = contexts.getContext(connectionCredentialsStateContext); const info = this.connectionInfoResource.get( createConnectionParam({ - id: data.state.connectionId, + id: data.state.config.connectionId!, projectId: data.state.projectId, }), ); @@ -160,6 +167,7 @@ export class ConnectionFormSSLPart extends FormPart { const { SSLTab } = await import('./SSLTab.js'); @@ -38,28 +25,22 @@ export const SSLPanel = React.lazy(async () => { @injectable() export class ConnectionSSLTabService extends Bootstrap { constructor( - private readonly connectionFormService: ConnectionFormService, private readonly dbDriverResource: DBDriverResource, private readonly networkHandlerResource: NetworkHandlerResource, - // private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, + private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, ) { super(); - - makeObservable(this, { - fillConfig: action, - prepareConfig: action, - }); } override register(): void { - this.connectionFormService.tabsContainer.add({ + this.connectionFormServiceRefactored.parts.add({ key: 'ssl', order: 4, tab: () => SSLTab, panel: () => SSLPanel, isHidden: (_, props) => { - if (props?.state.config.driverId) { - const driver = this.dbDriverResource.get(props.state.config.driverId); + if (props?.formState.state.config.driverId) { + const driver = this.dbDriverResource.get(props?.formState.state.config.driverId); const handler = getSSLDriverHandler(this.networkHandlerResource.values, driver?.applicableNetworkHandlers ?? []); return !handler; } @@ -67,192 +48,5 @@ export class ConnectionSSLTabService extends Bootstrap { return true; }, }); - - // TODO uncomment once refactored - // this.connectionFormServiceRefactored.parts.add({ - // key: 'ssl', - // order: 4, - // tab: () => SSLTab, - // panel: () => SSLPanel, - // isHidden: (_, props) => { - // if (props?.formState.state.driverId) { - // const driver = this.dbDriverResource.get(props.formState.state.driverId); - // const handler = getSSLDriverHandler(this.networkHandlerResource.values, driver?.applicableNetworkHandlers ?? []); - // return !handler; - // } - - // return true; - // }, - // }); - - this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); - - this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)); - - this.connectionFormService.configureTask.addHandler(this.configure.bind(this)); - - this.connectionFormService.fillConfigTask.addHandler(this.fillConfig.bind(this)); - } - - private async fillConfig({ state, updated }: IConnectionFormFillConfigData, contexts: IExecutionContextProvider) { - if (!updated || !state.config.driverId) { - return; - } - - const driver = await this.dbDriverResource.load(state.config.driverId); - const handlers = await this.networkHandlerResource.load(CachedMapAllKey); - - const handler = getSSLDriverHandler(handlers, driver?.applicableNetworkHandlers ?? []); - - if (!handler) { - return; - } - - const initialConfig = state.info?.networkHandlersConfig?.find(h => h.id === handler.id); - - if (!state.config.networkHandlersConfig) { - state.config.networkHandlersConfig = []; - } - - if (!state.config.networkHandlersConfig.some(state => state.id === handler.id)) { - const config: NetworkHandlerConfigInput = initialConfig ? toJS(initialConfig) : getSSLDefaultConfig(handler.id); - - if (config.secureProperties) { - config.properties = { ...config.properties, ...config.secureProperties }; - } - - state.config.networkHandlersConfig.push(config); - } - } - - // TODO make a solution to include all required data - private configure(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const configuration = contexts.getContext(connectionFormConfigureContext); - - configuration.include('includeNetworkHandlersConfig'); - } - - private async prepareConfig({ state, submitType }: IConnectionFormSubmitData, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - const credentialsState = contexts.getContext(connectionCredentialsStateContext); - - if (!state.config.networkHandlersConfig || state.config.networkHandlersConfig.length === 0 || !state.config.driverId) { - return; - } - - const driver = await this.dbDriverResource.load(state.config.driverId); - const handlers = await this.networkHandlerResource.load(CachedMapAllKey); - const handler = state.config.networkHandlersConfig.find( - handler => driver?.applicableNetworkHandlers.includes(handler.id) && handlers.some(h => h.id === handler.id && h.codeName === SSL_CODE_NAME), - ); - const descriptor = handlers.find(h => h.id === handler?.id); - - if (!handler) { - return; - } - - const initial = state.info?.networkHandlersConfig?.find(h => h.id === handler.id); - const handlerConfig: NetworkHandlerConfigInput = toJS(handler); - handlerConfig.savePassword = handler.savePassword || config.sharedCredentials; - - const changed = this.isChanged(handlerConfig, initial); - - if (changed && descriptor) { - for (const descriptorProperty of descriptor.properties) { - if (!descriptorProperty.id) { - continue; - } - - const key = descriptorProperty.id; - const isDefault = isNotNullDefined(descriptorProperty.defaultValue); - - if (!(key in handlerConfig.properties) && isDefault) { - handlerConfig.properties[key] = descriptorProperty.defaultValue; - } - - const secured = descriptorProperty.features.includes(PROPERTY_FEATURE_SECURED); - - if (secured) { - const value = handlerConfig.properties[key]; - const propertyChanged = initial?.secureProperties?.[key] !== value; - - if (propertyChanged) { - handlerConfig.secureProperties[key] = toJS(value); - } else { - delete handlerConfig.secureProperties[key]; - } - - delete handlerConfig.properties[key]; - } - } - - if (submitType === 'submit') { - if (Object.keys(handlerConfig.secureProperties).length === 0) { - delete handlerConfig.secureProperties; - } - - if (Object.keys(handlerConfig.properties).length === 0) { - delete handlerConfig.properties; - } - } - } - - if (handler.enabled && !handler.savePassword) { - credentialsState.requireNetworkHandler(handler.id); - } - - if (changed) { - if (!config.networkHandlersConfig) { - config.networkHandlersConfig = []; - } - - this.trimSSLConfig(handlerConfig); - config.networkHandlersConfig.push(handlerConfig); - } - } - - private trimSSLConfig(input: NetworkHandlerConfigInput) { - const { secureProperties } = input; - - if (!secureProperties) { - return; - } - - if (!Object.keys(secureProperties).length) { - return; - } - - for (const key in secureProperties) { - if (typeof secureProperties[key] === 'string') { - secureProperties[key] = secureProperties[key]?.trim(); - } - } - } - - private formState(data: IConnectionFormState, contexts: IExecutionContextProvider) { - const config = contexts.getContext(connectionConfigContext); - if (config.networkHandlersConfig !== undefined) { - const stateContext = contexts.getContext(formStateContext); - - stateContext.markEdited(); - } - } - - private isChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { - if (!initial && !handler.enabled) { - return false; - } - - const initialProperties = { ...(initial?.properties ?? {}), ...(initial?.secureProperties ?? {}) }; - - if ( - handler.enabled !== initial?.enabled || - handler.savePassword !== initial?.savePassword || - !isObjectsEqual(handler.properties, initialProperties) - ) { - return true; - } - - return false; } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx index 65698f3066..4e162e3db3 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx @@ -27,21 +27,23 @@ import { useService } from '@cloudbeaver/core-di'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import type { NetworkHandlerConfigInput, NetworkHandlerDescriptor } from '@cloudbeaver/core-sdk'; -import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; +import type { IFormState, TabContainerPanelComponent } from '@cloudbeaver/core-ui'; import { isSafari } from '@cloudbeaver/core-utils'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; import { SAVED_VALUE_INDICATOR } from './SAVED_VALUE_INDICATOR.js'; import styles from './SSL.module.css'; +import type { IConnectionFormStateRefactored } from '../IConnectionFormStateRefactored.js'; +import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; -interface Props extends IConnectionFormProps { +interface Props { handler: NetworkHandlerDescriptor; handlerState: NetworkHandlerConfigInput; + formState: IFormState; + sharedCredentials: boolean; + template: boolean; } -export const SSL: TabContainerPanelComponent = observer(function SSL({ state: formState, handler, handlerState }) { - const { info, readonly, disabled: formDisabled, loading } = formState; - +export const SSL: TabContainerPanelComponent = observer(function SSL({ formState, sharedCredentials, template, handler, handlerState }) { const translate = useTranslate(); const style = useS(styles); @@ -49,25 +51,20 @@ export const SSL: TabContainerPanelComponent = observer(function SSL({ st const { categories, isUncategorizedExists } = useObjectPropertyCategories(handler.properties); const serverConfigResource = useResource(SSL, ServerConfigResource, undefined); - const disabled = formDisabled || loading; + const disabled = formState.isDisabled; const enabled = handlerState.enabled || false; + const connectionInfoService = useService(ConnectionInfoResource); + const info = connectionInfoService.get(createConnectionParam(formState.state.projectId, formState.state.config.connectionId!)); const initialHandler = info?.networkHandlersConfig?.find(h => h.id === handler.id); const autofillToken = isSafari ? 'section-connection-authentication-ssl section-ssl' : 'new-password'; const projectInfoResource = useService(ProjectInfoResource); - const isSharedProject = projectInfoResource.isProjectShared(formState.projectId); + const isSharedProject = projectInfoResource.isProjectShared(formState.state.projectId); return ( - + {translate('plugin_connections_connection_ssl_enable')} {isUncategorizedExists && ( @@ -75,7 +72,7 @@ export const SSL: TabContainerPanelComponent = observer(function SSL({ st state={handlerState.properties} properties={handler.properties} category={null} - disabled={disabled || readonly || !enabled} + disabled={disabled || !enabled} isSaved={p => !!p.id && initialHandler?.secureProperties[p.id] === SAVED_VALUE_INDICATOR} autofillToken={autofillToken} hideEmptyPlaceholder @@ -91,7 +88,7 @@ export const SSL: TabContainerPanelComponent = observer(function SSL({ st state={handlerState.properties} properties={handler.properties} category={category} - disabled={disabled || readonly || !enabled} + disabled={disabled || !enabled} isSaved={p => !!p.id && initialHandler?.secureProperties[p.id] === SAVED_VALUE_INDICATOR} autofillToken={autofillToken} hideEmptyPlaceholder @@ -101,12 +98,12 @@ export const SSL: TabContainerPanelComponent = observer(function SSL({ st ))} - {credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && ( + {credentialsSavingEnabled && !template && !sharedCredentials && ( = observer(function SSLPanel(props) { +export const SSLPanel: TabContainerTabComponent = observer(function SSLPanel(props) { const networkHandlerResource = useResource(SSLPanel, NetworkHandlerResource, CachedMapAllKey); - const dbDriverResource = useResource(SSLPanel, DBDriverResource, props.state.config.driverId ?? null); + const dbDriverResource = useResource(SSLPanel, DBDriverResource, props.formState.state.config.driverId ?? null); + const SSLPart = getConnectionFormSSLPart(props.formState); const handler = getSSLDriverHandler(networkHandlerResource.resource.values, dbDriverResource.data?.applicableNetworkHandlers ?? []); - if (props.state.configured && handler && !props.state.config.networkHandlersConfig?.some(state => state.id === handler?.id)) { - props.state.config.networkHandlersConfig?.push(getSSLDefaultConfig(handler.id)); + if (SSLPart.isLoaded() && handler && !props.formState.state.config.networkHandlersConfig?.some(state => state.id === handler?.id)) { + props.formState.state.config.networkHandlersConfig?.push(getSSLDefaultConfig(handler.id)); } - const handlerState = props.state.config.networkHandlersConfig?.find(h => h.id === handler?.id); + const handlerState = props.formState.state.config.networkHandlersConfig?.find(h => h.id === handler?.id); + + useAutoLoad(SSLPanel, [SSLPart]); if (!handler || !handlerState) { return null; } - return ; + return ( + + ); }); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLTab.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLTab.tsx index bcde1ab239..ff778ea54f 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLTab.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSLTab.tsx @@ -12,12 +12,12 @@ import { DBDriverResource, NetworkHandlerResource } from '@cloudbeaver/core-conn import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { Tab, type TabContainerTabComponent, TabTitle } from '@cloudbeaver/core-ui'; -import type { IConnectionFormProps } from '../IConnectionFormProps.js'; import { getSSLDriverHandler } from './getSSLDriverHandler.js'; +import type { ConnectionFormRefactoredProps } from '../ConnectionFormServiceRefactored.js'; -export const SSLTab: TabContainerTabComponent = observer(function SSLTab(props) { +export const SSLTab: TabContainerTabComponent = observer(function SSLTab(props) { const networkHandlerResource = useResource(SSLTab, NetworkHandlerResource, CachedMapAllKey); - const dbDriverResource = useResource(SSLTab, DBDriverResource, props.state.config.driverId ?? null); + const dbDriverResource = useResource(SSLTab, DBDriverResource, props.formState.state.config.driverId ?? null); const handler = getSSLDriverHandler(networkHandlerResource.resource.values, dbDriverResource.data?.applicableNetworkHandlers ?? []); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/connectionFormConfigureContext.ts b/webapp/packages/plugin-connections/src/ConnectionForm/connectionFormConfigureContext.ts index d59940ffe3..b1d6a786fe 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/connectionFormConfigureContext.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/connectionFormConfigureContext.ts @@ -6,33 +6,25 @@ * you may not use this file except in compliance with the License. */ import type { DatabaseConnection } from '@cloudbeaver/core-connections'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import type { CachedResourceIncludeArgs } from '@cloudbeaver/core-resource'; import type { GetUserConnectionsQueryVariables } from '@cloudbeaver/core-sdk'; -import type { IConnectionFormState } from './IConnectionFormProps.js'; - export type ConnectionFormInfoIncludes = CachedResourceIncludeArgs; export interface IConnectionFormConfigureContext { - readonly driverId: string | undefined; - readonly info: DatabaseConnection | undefined; readonly connectionIncludes: ConnectionFormInfoIncludes; include: (...includes: ConnectionFormInfoIncludes) => any; } -export function connectionFormConfigureContext( - contexts: IExecutionContextProvider, - state: IConnectionFormState, -): IConnectionFormConfigureContext { +export function connectionFormConfigureContext(): IConnectionFormConfigureContext { return { - info: state.info, - driverId: state.config.driverId, connectionIncludes: [], include(...includes) { + const includedSet = new Set(this.connectionIncludes); + for (const include of includes) { - if (!this.connectionIncludes.includes(include as never)) { + if (!includedSet.has(include as never)) { this.connectionIncludes.push(include as never); } } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormState.ts deleted file mode 100644 index 111e597d22..0000000000 --- a/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormState.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 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 { useEffect, useState } from 'react'; - -import { ConnectionInfoOriginResource, ConnectionInfoResource } from '@cloudbeaver/core-connections'; -import { useService } from '@cloudbeaver/core-di'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; - -import { ConnectionFormService } from './ConnectionFormService.js'; -import { ConnectionFormState } from './ConnectionFormState.js'; -import type { IConnectionFormState } from './IConnectionFormProps.js'; - -export function useConnectionFormState( - resource: ConnectionInfoResource, - originResource: ConnectionInfoOriginResource, - configure?: (state: IConnectionFormState) => any, -): IConnectionFormState { - const projectsService = useService(ProjectsService); - const projectInfoResource = useService(ProjectInfoResource); - - const service = useService(ConnectionFormService); - const [state] = useState(() => { - const state = new ConnectionFormState(projectsService, projectInfoResource, service, resource, originResource); - configure?.(state); - - state.load(); - return state; - }); - - useEffect(() => () => state.dispose(), []); - - return state; -} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormStateRefactored.ts b/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormStateRefactored.ts index cba74eeb8e..1f46e1afbd 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormStateRefactored.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/useConnectionFormStateRefactored.ts @@ -10,24 +10,33 @@ import { IServiceProvider, useService } from '@cloudbeaver/core-di'; import { ConnectionFormStateRefactored } from './ConnectionFormStateRefactored.js'; import { ConnectionFormServiceRefactored } from './ConnectionFormServiceRefactored.js'; import type { IConnectionFormStateRefactored } from './IConnectionFormStateRefactored.js'; +import type { IConnectionInfoParams } from '@cloudbeaver/core-connections'; -const EMPTY_CONNECTION_INFO_PARAMS: IConnectionFormStateRefactored = { projectId: '', connectionId: '', driverId: '', submitType: 'submit' }; +const EMPTY_CONNECTION_INFO_PARAMS: IConnectionFormStateRefactored = { + projectId: '', + config: {}, + submitType: 'submit', + availableDrivers: [], + type: 'admin', +}; -// TODO is nullable return allowed? export function useConnectionFormStateRefactored( - state: IConnectionFormStateRefactored | null, + params: IConnectionInfoParams, configure?: (state: ConnectionFormStateRefactored) => any, -): ConnectionFormStateRefactored | null { +): ConnectionFormStateRefactored { const serviceProvider = useService(IServiceProvider); const service = useService(ConnectionFormServiceRefactored); - const ref = useRef(null); + const ref = useRef(null); - if ( - ref.current?.state.connectionId !== state?.connectionId || - ref.current?.state.projectId !== state?.projectId || - ref.current?.state.driverId !== state?.driverId - ) { - ref.current = new ConnectionFormStateRefactored(serviceProvider, service, state || EMPTY_CONNECTION_INFO_PARAMS); + if (ref.current?.state.config.connectionId !== params.connectionId || ref.current?.state.projectId !== params.projectId) { + ref.current = new ConnectionFormStateRefactored(serviceProvider, service, { + ...EMPTY_CONNECTION_INFO_PARAMS, + projectId: params.projectId, + config: { + ...EMPTY_CONNECTION_INFO_PARAMS.config, + connectionId: params.connectionId, + }, + }); configure?.(ref.current); } diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts index 5fbdcfc085..82c1859409 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts @@ -9,26 +9,18 @@ import { action, makeObservable, observable } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; -import { - ConnectionInfoOriginResource, - ConnectionInfoResource, - ConnectionsManagerService, - createConnectionParam, - type IConnectionInfoParams, -} from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { ConnectionInfoResource, ConnectionsManagerService, createConnectionParam, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { executorHandlerFilter, ExecutorInterrupter, type IExecutorHandler } from '@cloudbeaver/core-executor'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; import type { ResourceKey, ResourceKeySimple } from '@cloudbeaver/core-resource'; import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; -import { OptionsPanelService } from '@cloudbeaver/core-ui'; +import { FormMode, OptionsPanelService } from '@cloudbeaver/core-ui'; import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; -import { ConnectionFormService } from '../ConnectionForm/ConnectionFormService.js'; -import { ConnectionFormState } from '../ConnectionForm/ConnectionFormState.js'; -import type { IConnectionFormState } from '../ConnectionForm/IConnectionFormProps.js'; +import { ConnectionFormStateRefactored } from '../ConnectionForm/ConnectionFormStateRefactored.js'; +import { ConnectionFormServiceRefactored } from '../ConnectionForm/ConnectionFormServiceRefactored.js'; const PublicConnectionForm = importLazyComponent(() => import('./PublicConnectionForm.js').then(m => m.PublicConnectionForm)); @@ -36,20 +28,18 @@ const formGetter = () => PublicConnectionForm; @injectable() export class PublicConnectionFormService { - formState: IConnectionFormState | null; + formState: ConnectionFormStateRefactored | null; constructor( private readonly commonDialogService: CommonDialogService, private readonly notificationService: NotificationService, private readonly optionsPanelService: OptionsPanelService, - private readonly connectionFormService: ConnectionFormService, + private readonly serviceProvider: IServiceProvider, + private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, private readonly connectionInfoResource: ConnectionInfoResource, private readonly connectionsManagerService: ConnectionsManagerService, private readonly userInfoResource: UserInfoResource, private readonly authenticationService: AuthenticationService, - private readonly projectsService: ProjectsService, - private readonly projectInfoResource: ProjectInfoResource, - private readonly connectionInfoOriginResource: ConnectionInfoOriginResource, ) { this.formState = null; this.optionsPanelService.closeTask.addHandler(this.closeHandler); @@ -85,23 +75,19 @@ export class PublicConnectionFormService { // } if (!this.formState) { - this.formState = new ConnectionFormState( - this.projectsService, - this.projectInfoResource, - this.connectionFormService, - this.connectionInfoResource, - this.connectionInfoOriginResource, - ); + this.formState = new ConnectionFormStateRefactored(this.serviceProvider, this.connectionFormServiceRefactored, { + projectId, + availableDrivers: availableDrivers ?? [], + config, + submitType: 'submit', + type: 'public', + }); - this.formState.closeTask.addHandler(this.close.bind(this, true)); + // TODO what do we do with this handler? + // this.formState.closeTask.addHandler(this.close.bind(this, true)); } - this.formState - .setOptions(config.connectionId ? 'edit' : 'create', 'public') - .setConfig(projectId, config) - .setAvailableDrivers(availableDrivers || []); - - this.formState.load(); + this.formState.setMode(config.connectionId ? FormMode.Edit : FormMode.Create); } async open(projectId: string, config: ConnectionConfig, availableDrivers?: string[]): Promise { @@ -134,8 +120,8 @@ export class PublicConnectionFormService { async save(): Promise { const key = - this.formState && this.formState.config.connectionId && this.formState.projectId !== null - ? createConnectionParam(this.formState.projectId, this.formState.config.connectionId) + this.formState && this.formState.state.config.connectionId && this.formState.state.projectId !== null + ? createConnectionParam(this.formState.state.projectId, this.formState.state.config.connectionId) : null; await this.close(true); @@ -146,21 +132,23 @@ export class PublicConnectionFormService { } private readonly closeRemoved: IExecutorHandler> = (data, contexts) => { - if (!this.formState || !this.formState.config.connectionId || this.formState.projectId === null) { + if (!this.formState || !this.formState.state.config.connectionId || this.formState.state.projectId === null) { return; } - if (!this.connectionInfoResource.has(createConnectionParam(this.formState.projectId, this.formState.config.connectionId))) { + if (!this.connectionInfoResource.has(createConnectionParam(this.formState.state.projectId, this.formState.state.config.connectionId))) { this.close(true); } }; private readonly closeDeleted: IExecutorHandler> = (data, contexts) => { - if (!this.formState || !this.formState.config.connectionId || this.formState.projectId === null) { + if (!this.formState || !this.formState.state.config.connectionId || this.formState.state.projectId === null) { return; } - if (this.connectionInfoResource.isIntersect(data, createConnectionParam(this.formState.projectId, this.formState.config.connectionId))) { + if ( + this.connectionInfoResource.isIntersect(data, createConnectionParam(this.formState.state.projectId, this.formState.state.config.connectionId)) + ) { this.close(true); } }; @@ -177,16 +165,14 @@ export class PublicConnectionFormService { if ( !this.formState || !this.optionsPanelService.isOpen(formGetter) || - (this.formState.config.connectionId && - this.formState.projectId !== null && - !this.connectionInfoResource.has(createConnectionParam(this.formState.projectId, this.formState.config.connectionId))) + (this.formState.state.config.connectionId && + this.formState.state.projectId !== null && + !this.connectionInfoResource.has(createConnectionParam(this.formState.state.projectId, this.formState.state.config.connectionId))) ) { return true; } - const state = await this.formState.checkFormState(); - - if (!state?.edited) { + if (!this.formState.isChanged) { return true; } @@ -219,7 +205,8 @@ export class PublicConnectionFormService { } private clearFormState() { - this.formState?.dispose(); + // TODO dispose the form once we have this API + // this.formState?.dispose(); this.formState = null; } } diff --git a/webapp/packages/plugin-connections/src/index.ts b/webapp/packages/plugin-connections/src/index.ts index 1fbdc9d416..38adcb4c1a 100644 --- a/webapp/packages/plugin-connections/src/index.ts +++ b/webapp/packages/plugin-connections/src/index.ts @@ -18,12 +18,11 @@ export * from './ConnectionForm/Contexts/connectionCredentialsStateContext.js'; export * from './ConnectionForm/ConnectionFormBaseActionsLoader.js'; export * from './ConnectionForm/connectionFormConfigureContext.js'; export * from './ConnectionForm/ConnectionFormLoader.js'; -export * from './ConnectionForm/ConnectionFormService.js'; -export * from './ConnectionForm/ConnectionFormState.js'; export * from './ConnectionForm/IConnectionFormProps.js'; export * from './ConnectionForm/IConnectionFormStateRefactored.js'; +export * from './ConnectionForm/ConnectionFormStateRefactored.js'; +export * from './ConnectionForm/useConnectionFormStateRefactored.js'; export * from './ConnectionForm/ConnectionFormServiceRefactored.js'; -export * from './ConnectionForm/useConnectionFormState.js'; export * from './ConnectionForm/SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.js'; export * from './ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.js'; export * from './ContextMenu/MENU_CONNECTION_VIEW.js'; diff --git a/webapp/packages/plugin-connections/src/manifest.ts b/webapp/packages/plugin-connections/src/manifest.ts index 8060b2f1f4..a113dc28dd 100644 --- a/webapp/packages/plugin-connections/src/manifest.ts +++ b/webapp/packages/plugin-connections/src/manifest.ts @@ -18,7 +18,6 @@ export const connectionPlugin: PluginManifest = { () => import('./PublicConnectionForm/PublicConnectionFormService.js').then(m => m.PublicConnectionFormService), () => import('./LocaleService.js').then(m => m.LocaleService), () => import('./ConnectionAuthService.js').then(m => m.ConnectionAuthService), - () => import('./ConnectionForm/ConnectionFormService.js').then(m => m.ConnectionFormService), () => import('./ConnectionForm/Options/ConnectionOptionsTabService.js').then(m => m.ConnectionOptionsTabService), () => import('./ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.js').then(m => m.ConnectionDriverPropertiesTabService), () => import('./ConnectionForm/SSH/ConnectionSSHTabService.js').then(m => m.ConnectionSSHTabService),