From 3f66dee8d770a728fa7208df514914ae6d184930 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Thu, 4 Feb 2021 16:44:38 +0300 Subject: [PATCH 1/9] CB-686 Test network handlers API --- .../META-INF/MANIFEST.MF | 3 +- .../schema/service.core.graphqls | 9 +++ .../model/WebNetworkEndpointInfo.java | 57 ++++++++++++++++++ .../service/core/DBWServiceCore.java | 3 + .../service/core/impl/WebServiceCore.java | 60 +++++++++++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkEndpointInfo.java diff --git a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF index 2c9ed519a7..10de64a779 100644 --- a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF @@ -24,7 +24,8 @@ Require-Bundle: org.eclipse.core.runtime;visibility:=reexport, org.eclipse.jetty.util;visibility:=reexport, com.google.gson;visibility:=reexport, org.jkiss.bundle.graphql.java;visibility:=reexport, - org.jkiss.bundle.apache.dbcp + org.jkiss.bundle.apache.dbcp, + org.jkiss.dbeaver.net.ssh Export-Package: io.cloudbeaver, io.cloudbeaver.auth.provider, io.cloudbeaver.auth.provider.local, diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 41843f2810..100740ce3c 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -251,6 +251,12 @@ type ConnectionInfo { supportedDataFormats: [ ResultDataFormat! ]! } +type NetworkEndpointInfo { + message: String + clientVersion: String + serverVersion: String +} + type ObjectOrigin { type: ID! subType: ID @@ -409,6 +415,9 @@ extend type Mutation { # Test connection configuration. Returns remote server version testConnection( config: ConnectionConfig! ): ConnectionInfo! + # Test connection configuration. Returns remote server version + testNetworkHandler( config: NetworkHandlerConfigInput! ): NetworkEndpointInfo! + # Initiate existing connection initConnection( id: ID!, credentials: Object, saveCredentials: Boolean ): ConnectionInfo! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkEndpointInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkEndpointInfo.java new file mode 100644 index 0000000000..c05e552bea --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebNetworkEndpointInfo.java @@ -0,0 +1,57 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model; + +/** + * WebNetworkEndpointInfo + */ +public class WebNetworkEndpointInfo { + + private String message; + private String clientVersion; + private String serverVersion; + + public WebNetworkEndpointInfo(String message) { + this.message = message; + } + + public WebNetworkEndpointInfo(String message, String clientVersion, String serverVersion) { + this.message = message; + this.clientVersion = clientVersion; + this.serverVersion = serverVersion; + } + + public String getMessage() { + return message; + } + + public String getClientVersion() { + return clientVersion; + } + + public void setClientVersion(String clientVersion) { + this.clientVersion = clientVersion; + } + + public String getServerVersion() { + return serverVersion; + } + + public void setServerVersion(String serverVersion) { + this.serverVersion = serverVersion; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index 59d77ab423..ffeb3be6ea 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -104,6 +104,9 @@ public interface DBWServiceCore extends DBWService { @WebAction WebConnectionInfo testConnection(WebSession webSession, WebConnectionConfig connectionConfig) throws DBWebException; + @WebAction + WebNetworkEndpointInfo testNetworkHandler(WebSession webSession, WebNetworkHandlerConfigInput nhConfig) throws DBWebException; + @WebAction WebConnectionInfo closeConnection(WebSession webSession, String connectionId) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index 59bb462e67..513ba834b7 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -38,9 +38,16 @@ import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.model.navigator.DBNDataSource; import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.navigator.DBNNode; +import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration; +import org.jkiss.dbeaver.model.net.DBWNetworkHandler; +import org.jkiss.dbeaver.model.net.DBWTunnel; +import org.jkiss.dbeaver.model.net.ssh.SSHImplementation; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.registry.DataSourceDescriptor; import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; +import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry; +import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob; import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; @@ -366,6 +373,59 @@ public class WebServiceCore implements DBWServiceCore { } } + @Override + public WebNetworkEndpointInfo testNetworkHandler(WebSession webSession, WebNetworkHandlerConfigInput nhConfig) throws DBWebException { + DBRProgressMonitor monitor = webSession.getProgressMonitor(); + monitor.beginTask("Instantiate SSH tunnel", 2); + + NetworkHandlerDescriptor handlerDescriptor = NetworkHandlerRegistry.getInstance().getDescriptor(nhConfig.getId()); + if (handlerDescriptor == null) { + throw new DBWebException("Network handler '" + nhConfig.getId() + "' not found"); + } + try { + DBWNetworkHandler handler = handlerDescriptor.createHandler(DBWNetworkHandler.class); + if (handler instanceof DBWTunnel) { + DBWTunnel tunnel = (DBWTunnel)handler; + DBPConnectionConfiguration connectionConfig = new DBPConnectionConfiguration(); + connectionConfig.setHostName("localhost"); + connectionConfig.setHostPort(CommonUtils.toString(nhConfig.getProperties().get(DBWHandlerConfiguration.PROP_PORT))); + try { + monitor.subTask("Initialize tunnel"); + + DBWHandlerConfiguration configuration = new DBWHandlerConfiguration(handlerDescriptor, null); + configuration.setUserName(nhConfig.getUserName()); + configuration.setPassword(nhConfig.getPassword()); + configuration.setSavePassword(true); + configuration.setEnabled(true); + configuration.setProperties(nhConfig.getProperties()); + tunnel.initializeHandler(monitor, DBWorkbench.getPlatform(), configuration, connectionConfig); + monitor.worked(1); + // Get info + Object implementation = tunnel.getImplementation(); + if (implementation instanceof SSHImplementation) { + return new WebNetworkEndpointInfo( + "Connected", + ((SSHImplementation) implementation).getClientVersion(), + ((SSHImplementation) implementation).getServerVersion()); + } else { + return new WebNetworkEndpointInfo("Connected"); + } + } finally { + monitor.subTask("Close tunnel"); + tunnel.closeTunnel(monitor); + monitor.worked(1); + } + } else { + return new WebNetworkEndpointInfo(nhConfig.getId() + " is not a tunnel"); + } + } catch (Exception e) { + throw new DBWebException("Error testing network handler endpoint", e); + } finally { + // Close it + monitor.done(); + } + } + @Override public WebConnectionInfo closeConnection(WebSession webSession, String connectionId) throws DBWebException { return closeAndDeleteConnection(webSession, connectionId, false); From fd0575b141e9b72b87a44f7f33d302a74750c7c5 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Thu, 4 Feb 2021 17:00:57 +0300 Subject: [PATCH 2/9] CB-686 Test network handlers API --- .../src/io/cloudbeaver/service/core/WebServiceBindingCore.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index 1ddc974e33..db6ed3aa2f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -22,6 +22,7 @@ import graphql.schema.idl.TypeRuntimeWiring; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebConnectionConfig; +import io.cloudbeaver.model.WebNetworkHandlerConfigInput; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionManager; import io.cloudbeaver.server.CBPlatform; @@ -89,6 +90,7 @@ public class WebServiceBindingCore extends WebServiceBindingBase new WebConnectionConfig(env.getArgument("config")))) .dataFetcher("initConnection", env -> getService(env).initConnection(getWebSession(env), env.getArgument("id"), env.getArgument("credentials"), env.getArgument("saveCredentials"))) .dataFetcher("testConnection", env -> getService(env).testConnection(getWebSession(env), getConnectionConfig(env))) + .dataFetcher("testNetworkHandler", env -> getService(env).testNetworkHandler(getWebSession(env), new WebNetworkHandlerConfigInput(env.getArgument("config")))) .dataFetcher("closeConnection", env -> getService(env).closeConnection(getWebSession(env), env.getArgument("id"))) .dataFetcher("deleteConnection", env -> getService(env).deleteConnection(getWebSession(env), env.getArgument("id"))) From c485f75f76097e2e3effe97602166b42906b4ebf Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Thu, 4 Feb 2021 17:11:03 +0300 Subject: [PATCH 3/9] CB-707 Public/admin creds save config --- samples/sample-databases/cloudbeaver.conf | 5 ++++- .../schema/service.core.graphqls | 4 ++++ .../io/cloudbeaver/model/WebServerConfig.java | 10 ++++++++++ .../io/cloudbeaver/server/CBAppConfig.java | 18 +++++++++++++++++ .../schema/service.admin.graphqls | 2 ++ .../service/admin/AdminServerConfig.java | 20 +++++++++---------- .../service/admin/impl/WebServiceAdmin.java | 2 ++ 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/samples/sample-databases/cloudbeaver.conf b/samples/sample-databases/cloudbeaver.conf index dbebc9eaea..323bad43f6 100644 --- a/samples/sample-databases/cloudbeaver.conf +++ b/samples/sample-databases/cloudbeaver.conf @@ -36,7 +36,10 @@ anonymousAccessEnabled: true, authenticationEnabled: true, anonymousUserRole: "user", - supportsCustomConnections: false + supportsCustomConnections: false, + + publicCredentialsSaveEnabled: true, + adminCredentialsSaveEnabled: true /* enabledDrivers: [ diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 100740ce3c..d9a8ed8359 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -84,6 +84,10 @@ type ServerConfig { supportsCustomConnections: Boolean supportsConnectionBrowser: Boolean supportsWorkspaces: Boolean + + publicCredentialsSaveEnabled: Boolean + adminCredentialsSaveEnabled: Boolean + sessionExpireTime: Int localHostAddress: String diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index cca0d4a67a..74872c38fd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -77,6 +77,16 @@ public class WebServerConfig { return application.getAppConfiguration().isSupportsUserWorkspaces(); } + @Property + public boolean isPublicCredentialsSaveEnabled() { + return application.getAppConfiguration().isPublicCredentialsSaveEnabled(); + } + + @Property + public boolean isAdminCredentialsSaveEnabled() { + return application.getAppConfiguration().isAdminCredentialsSaveEnabled(); + } + @Property public boolean isConfigurationMode() { return application.isConfigurationMode(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java index 1b72a81742..616ff171b6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java @@ -33,6 +33,8 @@ public class CBAppConfig { private boolean supportsCustomConnections = true; private boolean supportsConnectionBrowser = false; private boolean supportsUserWorkspaces = false; + private boolean publicCredentialsSaveEnabled = true; + private boolean adminCredentialsSaveEnabled = true; private String[] enabledDrivers = new String[0]; private String[] disabledDrivers = new String[0]; private String defaultAuthProvider = LocalAuthProvider.PROVIDER_ID; @@ -78,6 +80,22 @@ public class CBAppConfig { return supportsUserWorkspaces; } + public boolean isPublicCredentialsSaveEnabled() { + return publicCredentialsSaveEnabled; + } + + public void setPublicCredentialsSaveEnabled(boolean publicCredentialsSaveEnabled) { + this.publicCredentialsSaveEnabled = publicCredentialsSaveEnabled; + } + + public boolean isAdminCredentialsSaveEnabled() { + return adminCredentialsSaveEnabled; + } + + public void setAdminCredentialsSaveEnabled(boolean adminCredentialsSaveEnabled) { + this.adminCredentialsSaveEnabled = adminCredentialsSaveEnabled; + } + public String[] getEnabledDrivers() { return enabledDrivers; } diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index 9b5c222370..533d427909 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -54,6 +54,8 @@ input ServerConfigInput { anonymousAccessEnabled: Boolean authenticationEnabled: Boolean customConnectionsEnabled: Boolean + publicCredentialsSaveEnabled: Boolean + adminCredentialsSaveEnabled: Boolean sessionExpireTime: Int } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java index d86f828243..f385e12817 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java @@ -35,6 +35,8 @@ public class AdminServerConfig { private boolean anonymousAccessEnabled; private boolean authenticationEnabled; private boolean customConnectionsEnabled; + private boolean publicCredentialsSaveEnabled; + private boolean adminCredentialsSaveEnabled; private long sessionExpireTime; @@ -47,6 +49,8 @@ public class AdminServerConfig { this.anonymousAccessEnabled = JSONUtils.getBoolean(params, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); this.authenticationEnabled = JSONUtils.getBoolean(params, "authenticationEnabled", appConfig.isAuthenticationEnabled()); this.customConnectionsEnabled = JSONUtils.getBoolean(params, "customConnectionsEnabled", appConfig.isSupportsCustomConnections()); + this.publicCredentialsSaveEnabled = JSONUtils.getBoolean(params, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); + this.adminCredentialsSaveEnabled = JSONUtils.getBoolean(params, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); this.sessionExpireTime = JSONUtils.getLong(params, "sessionExpireTime", -1); } @@ -79,24 +83,20 @@ public class AdminServerConfig { return anonymousAccessEnabled; } - public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) { - this.anonymousAccessEnabled = anonymousAccessEnabled; - } - public boolean isAuthenticationEnabled() { return authenticationEnabled; } - public void setAuthenticationEnabled(boolean authenticationEnabled) { - this.authenticationEnabled = authenticationEnabled; - } - public boolean isCustomConnectionsEnabled() { return customConnectionsEnabled; } - public void setCustomConnectionsEnabled(boolean customConnectionsEnabled) { - this.customConnectionsEnabled = customConnectionsEnabled; + public boolean isPublicCredentialsSaveEnabled() { + return publicCredentialsSaveEnabled; + } + + public boolean isAdminCredentialsSaveEnabled() { + return adminCredentialsSaveEnabled; } public long getSessionExpireTime() { diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index 63d97ee434..d5ad95ea68 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -350,6 +350,8 @@ public class WebServiceAdmin implements DBWServiceAdmin { appConfig.setAnonymousAccessEnabled(config.isAnonymousAccessEnabled()); appConfig.setAuthenticationEnabled(config.isAuthenticationEnabled()); appConfig.setSupportsCustomConnections(config.isCustomConnectionsEnabled()); + appConfig.setPublicCredentialsSaveEnabled(config.isPublicCredentialsSaveEnabled()); + appConfig.setAdminCredentialsSaveEnabled(config.isAdminCredentialsSaveEnabled()); String adminName = config.getAdminName(); String adminPassword = config.getAdminPassword(); if (CommonUtils.isEmpty(adminName)) { From fc6b65b4ed322cd60f67927aa6334f51c1a81462 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Thu, 4 Feb 2021 17:47:29 +0300 Subject: [PATCH 4/9] CB-502 Derby server driver config fixed --- server/drivers/derby/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/drivers/derby/pom.xml b/server/drivers/derby/pom.xml index 4867e4073f..e54b0163f3 100644 --- a/server/drivers/derby/pom.xml +++ b/server/drivers/derby/pom.xml @@ -30,6 +30,11 @@ derbyshared 10.15.1.3 + + org.apache.derby + derbyclient + 10.15.1.3 + From 0627f55148715b60b87960286b4c8189ad8c6504 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 4 Feb 2021 17:58:11 +0300 Subject: [PATCH 5/9] feat(core-connections): CB-692 add new custom properties --- .../src/FormControls/Checkboxes/Checkbox.tsx | 13 ++++++- .../Checkboxes/CheckboxMarkup.tsx | 6 ++-- .../ObjectPropertyInfoForm.tsx | 21 +++++++++-- .../ConnectionFormController.ts | 13 +++++-- .../ConnectionForm/Options/Options.tsx | 14 +++++++- .../Options/OptionsController.ts | 7 +++- .../ConnectionForm/OriginInfo/OriginInfo.tsx | 4 +-- .../src/Administration/ConnectionsResource.ts | 1 + .../core-connections/src/DBDriverResource.ts | 31 ++++++++-------- .../DatabaseAuthDialog/DatabaseAuthDialog.tsx | 2 +- .../core-connections/src/locales/en.ts | 1 + .../core-connections/src/locales/ru.ts | 1 + .../src/queries/connections/driverList.gql | 14 +++++++- .../src/queries/fragments/AdminConnection.gql | 1 + webapp/packages/core-sdk/src/sdk.ts | 36 +++++++++++++++++-- .../Users/UserForm/OriginInfo.tsx | 2 +- .../ConnectionForm/ConnectionForm.tsx | 2 +- .../src/ConnectionDialog/ConnectionDialog.tsx | 2 +- 18 files changed, 136 insertions(+), 35 deletions(-) diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx index 7e5194fe7c..a683c9757b 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx @@ -17,6 +17,7 @@ import { CheckboxMarkup } from './CheckboxMarkup'; export type CheckboxBaseProps = Omit, 'onChange' | 'type' | 'value' | 'checked'> & { value?: string; checkboxLabel?: string; + title?: string; mod?: 'surface'; long?: boolean; }; @@ -52,6 +53,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ value, state, checkboxLabel, + title, checked: checkedControlled, children, className, @@ -89,7 +91,15 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ return null; } - let checked = state ? state[name] : checkedControlled; + let checked = checkedControlled; + + if (state) { + if (typeof state[name] === 'string') { + checked = state[name] === 'true'; + } else { + checked = state[name]; + } + } if (Array.isArray(checked)) { checked = checked.includes(value); @@ -102,6 +112,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ id={value || name} checked={checked} label={checkboxLabel} + title={title} className={className} onChange={handleChange} {...use({ mod })} diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx index 89976b56d8..2e099a231d 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx @@ -78,10 +78,12 @@ interface ICheckboxMarkupProps extends React.InputHTMLAttributes = function CheckboxMarkup({ label, className, ...rest }) { +export const CheckboxMarkup: React.FC = function CheckboxMarkup({ + label, className, title, ...rest +}) { return styled(useStyles(checkboxStyles, checkboxMod.primary, rest.disabled && checkboxState.disabled, rest.checked && checkboxState.checked))( - + diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx index 27d3a34fa0..971ab34f69 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx @@ -14,6 +14,7 @@ import { InputField } from '@cloudbeaver/core-blocks'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; +import { FieldCheckbox } from '../FormControls/Checkboxes/FieldCheckbox'; import { FormFieldDescription } from '../FormControls/FormFieldDescription'; import { FormGroup } from '../FormControls/FormGroup'; import { isControlPresented } from '../FormControls/isControlPresented'; @@ -48,6 +49,7 @@ const RenderField: React.FC = observer(function RenderField({ }) { const href = property.features.includes('href'); const password = property.features.includes('password'); + const checkbox = property.dataType === 'Boolean'; let description: string | undefined; if (href) { @@ -73,6 +75,19 @@ const RenderField: React.FC = observer(function RenderField({ description = 'Password saved'; } + if (checkbox) { + return ( + + ); + } + return ( = observer(function RenderField({ interface ObjectPropertyFormProps { properties: ObjectPropertyInfo[] | undefined; - credentials: Record; + state: Record; editable?: boolean; autofillToken?: string; className?: string; @@ -106,7 +121,7 @@ interface ObjectPropertyFormProps { export const ObjectPropertyInfoForm: React.FC = observer(function ObjectPropertyInfoForm({ properties, - credentials, + state, editable = true, autofillToken = '', className, @@ -134,7 +149,7 @@ export const ObjectPropertyInfoForm: React.FC = observe ; @@ -80,7 +80,10 @@ implements IInitializableController { const connectionConfig = this.getConnectionConfig(); const validation = this.validate(connectionConfig); if (!validation.status) { - this.notificationService.logError({ title: this.model.editing ? 'connections_administration_connection_save_error' : 'connections_administration_connection_create_error', message: validation.errorMessage }); + this.notificationService.logError({ + title: this.model.editing ? 'connections_administration_connection_save_error' : 'connections_administration_connection_create_error', + message: validation.errorMessage, + }); return; } @@ -133,7 +136,7 @@ implements IInitializableController { if (!config.name?.length) { validationStatus.errorMessage = "Field 'name' can't be empty"; } else if (this.model.editing && this.isConnectionNameAlreadyExists(config.name)) { - validationStatus.errorMessage = 'Connection with this name already exists'; + validationStatus.errorMessage = `Connection with ${config.name} name already exists`; } validationStatus.status = !validationStatus.errorMessage; @@ -173,6 +176,10 @@ implements IInitializableController { } config.networkHandlersConfig = this.model.networkHandlersState; + if (Object.keys(this.model.connection.providerProperties).length > 0) { + config.providerProperties = this.model.connection.providerProperties; + } + return config; } diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx index 39b37ca05c..d27e354225 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/Options.tsx @@ -140,7 +140,7 @@ export const Options = observer(function Options({ @@ -157,6 +157,18 @@ export const Options = observer(function Options({ )} + {controller.providerProperties && controller.providerProperties.length > 0 && ( + + + {translate('connections_connection_edit_settings')} + + + + )} ); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts index 75f3647aa6..bcb009a71d 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts @@ -18,7 +18,7 @@ import type { IConnectionFormModel } from '../IConnectionFormModel'; @injectable() export class OptionsController -implements IInitializableController { + implements IInitializableController { get drivers(): DBDriver[] { return Array.from(this.dbDriverResource.data.values()) .filter(({ id }) => this.model.availableDrivers.includes(id)); @@ -46,6 +46,10 @@ implements IInitializableController { return this.authModel?.properties; } + get providerProperties(): ObjectPropertyInfo[] | undefined { + return this.dbDriverResource.get(this.model.connection.driverId)?.providerProperties; + } + private model!: IConnectionFormModel; /** we want to save prev generated connection name to detect if user changed "name" field to turn off autofill */ private prevGeneratedName: string | null = null; @@ -87,6 +91,7 @@ implements IInitializableController { this.setDefaultParameters(prevDriverId); this.cleanCredentials(); this.model.connection.properties = {}; + this.model.connection.providerProperties = {}; this.model.connection.authModel = this.driver?.defaultAuthModel; } diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/OriginInfo/OriginInfo.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/OriginInfo/OriginInfo.tsx index f8334ed9a3..d1b7cad108 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/OriginInfo/OriginInfo.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/OriginInfo/OriginInfo.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; -import { TextPlaceholder, useTab, ObjectPropertyInfoForm, FormBox, FormBoxElement, FormGroup, InputGroup, Loader, useTabState, ExceptionMessage } from '@cloudbeaver/core-blocks'; +import { TextPlaceholder, useTab, ObjectPropertyInfoForm, FormBox, FormBoxElement, FormGroup, Loader, useTabState, ExceptionMessage } from '@cloudbeaver/core-blocks'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; @@ -94,7 +94,7 @@ export const OriginInfo: TabContainerPanelComponent = obse
diff --git a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts index 1128a1d396..0d4474df9f 100644 --- a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts +++ b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts @@ -71,6 +71,7 @@ export class ConnectionsResource extends CachedMapResource; @injectable() diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx index 84baa30a99..eb76bfd2b0 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx @@ -101,7 +101,7 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({ diff --git a/webapp/packages/core-connections/src/locales/en.ts b/webapp/packages/core-connections/src/locales/en.ts index 2e4f1a2e6a..4b56ea518d 100644 --- a/webapp/packages/core-connections/src/locales/en.ts +++ b/webapp/packages/core-connections/src/locales/en.ts @@ -16,6 +16,7 @@ export default [ ['connections_connection_create_search_database', 'Search'], ['connections_connection_edit_save_credentials', 'Save credentials'], ['connections_connection_edit_authentication', 'Authentication'], + ['connections_connection_edit_settings', 'Settings'], ['connections_connection_edit_access', 'Access'], ['connections_connection_edit_access_load_failed', 'Fail to get connection access'], ['connections_connection_edit_access_role', 'Role'], diff --git a/webapp/packages/core-connections/src/locales/ru.ts b/webapp/packages/core-connections/src/locales/ru.ts index 21a486c6b9..c7989d69c7 100644 --- a/webapp/packages/core-connections/src/locales/ru.ts +++ b/webapp/packages/core-connections/src/locales/ru.ts @@ -15,6 +15,7 @@ export default [ ['connections_connection_create_search_database', 'Найти'], ['connections_connection_edit_save_credentials', ' Запомнить данные'], ['connections_connection_edit_authentication', 'Авторизация'], + ['connections_connection_edit_settings', 'Настройки'], ['connections_connection_edit_access', 'Доступ'], ['connections_connection_edit_access_load_failed', 'Не удалось загрузить информацию доступа'], ['connections_connection_edit_access_role', 'Роль'], diff --git a/webapp/packages/core-sdk/src/queries/connections/driverList.gql b/webapp/packages/core-sdk/src/queries/connections/driverList.gql index db52518e43..6586da9549 100644 --- a/webapp/packages/core-sdk/src/queries/connections/driverList.gql +++ b/webapp/packages/core-sdk/src/queries/connections/driverList.gql @@ -15,6 +15,18 @@ query driverList { defaultAuthModel # applicableAuthModel + + providerProperties { + id + displayName + description + category + dataType + defaultValue + validValues + features + order + } applicableNetworkHandlers } -} \ No newline at end of file +} diff --git a/webapp/packages/core-sdk/src/queries/fragments/AdminConnection.gql b/webapp/packages/core-sdk/src/queries/fragments/AdminConnection.gql index 5edf54eec1..9774a0947e 100644 --- a/webapp/packages/core-sdk/src/queries/fragments/AdminConnection.gql +++ b/webapp/packages/core-sdk/src/queries/fragments/AdminConnection.gql @@ -15,6 +15,7 @@ fragment AdminConnection on ConnectionInfo { databaseName url properties + providerProperties features diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 2fb09d54d9..fe7ef5d88c 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -268,6 +268,7 @@ export interface Mutation { sqlContextSetDefaults: Scalars['Boolean']; sqlResultClose: Scalars['Boolean']; testConnection: ConnectionInfo; + testNetworkHandler: NetworkEndpointInfo; touchSession?: Maybe; updateResultsData?: Maybe; updateResultsDataBatch?: Maybe; @@ -374,6 +375,10 @@ export interface MutationTestConnectionArgs { config: ConnectionConfig; } +export interface MutationTestNetworkHandlerArgs { + config: NetworkHandlerConfigInput; +} + export interface MutationUpdateResultsDataArgs { connectionId: Scalars['ID']; contextId: Scalars['ID']; @@ -448,6 +453,8 @@ export interface ServerConfig { supportsCustomConnections?: Maybe; supportsConnectionBrowser?: Maybe; supportsWorkspaces?: Maybe; + publicCredentialsSaveEnabled?: Maybe; + adminCredentialsSaveEnabled?: Maybe; sessionExpireTime?: Maybe; localHostAddress?: Maybe; configurationMode?: Maybe; @@ -569,6 +576,12 @@ export interface ConnectionInfo { supportedDataFormats: ResultDataFormat[]; } +export interface NetworkEndpointInfo { + message?: Maybe; + clientVersion?: Maybe; + serverVersion?: Maybe; +} + export interface ObjectOrigin { type: Scalars['ID']; subType?: Maybe; @@ -844,6 +857,8 @@ export interface ServerConfigInput { anonymousAccessEnabled?: Maybe; authenticationEnabled?: Maybe; customConnectionsEnabled?: Maybe; + publicCredentialsSaveEnabled?: Maybe; + adminCredentialsSaveEnabled?: Maybe; sessionExpireTime?: Maybe; } @@ -1097,7 +1112,12 @@ export type DeleteConnectionMutation = Pick; export type DriverListQueryVariables = Exact<{ [key: string]: never }>; -export interface DriverListQuery { driverList: Array> } +export interface DriverListQuery { + driverList: Array<( + Pick + & { providerProperties: Array> } + )>; +} export type DriverPropertiesQueryVariables = Exact<{ driverId: Scalars['ID']; @@ -1216,7 +1236,7 @@ export type NavGetStructContainersQueryVariables = Exact<{ export interface NavGetStructContainersQuery { navGetStructContainers: { catalogList: Array>; schemaList: Array> } } export type AdminConnectionFragment = ( - Pick + Pick & { origin: ObjectOriginInfoFragment; authProperties: UserConnectionAuthPropertiesFragment[]; networkHandlersConfig: Array> } ); @@ -1540,6 +1560,7 @@ export const AdminConnectionFragmentDoc = ` databaseName url properties + providerProperties features origin { ...ObjectOriginInfo @@ -1907,6 +1928,17 @@ export const DriverListDocument = ` anonymousAccess promotedScore defaultAuthModel + providerProperties { + id + displayName + description + category + dataType + defaultValue + validValues + features + order + } applicableNetworkHandlers } } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/OriginInfo.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/OriginInfo.tsx index ba83f15723..6240bf3a79 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/OriginInfo.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/OriginInfo.tsx @@ -98,7 +98,7 @@ export const OriginInfo: TabContainerPanelComponent = observer(f
diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/ConnectionForm/ConnectionForm.tsx b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/ConnectionForm/ConnectionForm.tsx index a0501672cd..4f28e4530d 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/ConnectionForm/ConnectionForm.tsx +++ b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/ConnectionForm/ConnectionForm.tsx @@ -89,7 +89,7 @@ export const ConnectionForm = observer(function ConnectionForm({ diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx index aa20964734..473f83a936 100644 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx +++ b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx @@ -85,7 +85,7 @@ export const ConnectionDialog = observer(function ConnectionDialog({ From a72c70dae74931e33d5c48a6c923b945f88f5a19 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 4 Feb 2021 18:36:50 +0300 Subject: [PATCH 6/9] feat(core-connections): CB-692 remove unnecessary type 'title' from checkbox component --- .../core-blocks/src/FormControls/Checkboxes/Checkbox.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx index a683c9757b..e54f653579 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx @@ -17,7 +17,6 @@ import { CheckboxMarkup } from './CheckboxMarkup'; export type CheckboxBaseProps = Omit, 'onChange' | 'type' | 'value' | 'checked'> & { value?: string; checkboxLabel?: string; - title?: string; mod?: 'surface'; long?: boolean; }; @@ -53,7 +52,6 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ value, state, checkboxLabel, - title, checked: checkedControlled, children, className, @@ -112,7 +110,6 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ id={value || name} checked={checked} label={checkboxLabel} - title={title} className={className} onChange={handleChange} {...use({ mod })} From 075c3242b183fd955be95c8c8a802725106ab83b Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Thu, 4 Feb 2021 19:40:28 +0300 Subject: [PATCH 7/9] CB-178 Datasource node refresh fix --- .../service/navigator/impl/WebServiceNavigator.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index e4e8b2ba27..bab90e7268 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -130,7 +130,13 @@ public class WebServiceNavigator implements DBWServiceNavigator { if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } - node.refreshNode(monitor, this); + if (node instanceof DBNDataSource) { + // Do not refresh entire tree - just clear child nodes + // Otherwise refresh may fail if navigator settings were changed. + ((DBNDataSource) node).cleanupNode(); + } else { + node.refreshNode(monitor, this); + } return true; } catch (DBException e) { throw new DBWebException("Error refreshing navigator node '" + nodePath + "'", e); From da956a152ca3a47f483de44c43fad4188ac36f60 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Thu, 4 Feb 2021 20:07:27 +0300 Subject: [PATCH 8/9] CB-707 Save creds save config in runtime props --- .../src/io/cloudbeaver/server/CBApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index 86222c972a..cb2fc0a394 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -573,6 +573,8 @@ public class CBApplication extends BaseApplicationImpl { JSONUtils.field(json, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); JSONUtils.field(json, "authenticationEnabled", appConfig.isAuthenticationEnabled()); JSONUtils.field(json, "supportsCustomConnections", appConfig.isSupportsCustomConnections()); + JSONUtils.field(json, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); + JSONUtils.field(json, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); if (!CommonUtils.isEmpty(appConfig.getPlugins())) { JSONUtils.serializeProperties(json, "plugins", appConfig.getPlugins()); From 2d8f0b4d6e61d5410c380923ead2fe0485447132 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 4 Feb 2021 20:56:11 +0300 Subject: [PATCH 9/9] feat(plugin-data-spreadsheet-new): CB-655 cell editor component --- .../src/shared/InlineEditor/styles.ts | 4 +- .../plugin-data-spreadsheet-new/package.json | 3 +- .../src/DataGrid/CellEditor/CellEditor.tsx | 95 +++++++++++++++++++ .../src/DataGrid/DataGridContext.ts | 5 +- .../src/DataGrid/DataGridTable.tsx | 33 ++++++- ...mnHeader-new.tsx => TableColumnHeader.tsx} | 7 +- .../src/styles/base.scss | 13 +++ webapp/yarn.lock | 20 ++++ 8 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx rename webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/{TableColumnHeader-new.tsx => TableColumnHeader.tsx} (93%) diff --git a/webapp/packages/core-app/src/shared/InlineEditor/styles.ts b/webapp/packages/core-app/src/shared/InlineEditor/styles.ts index 7148842947..59ee047403 100644 --- a/webapp/packages/core-app/src/shared/InlineEditor/styles.ts +++ b/webapp/packages/core-app/src/shared/InlineEditor/styles.ts @@ -53,8 +53,8 @@ export const InlineEditorStyles = composes( letter-spacing: inherit; outline: none; border: none; - padding-left: 11px; /* because of left border */ - padding-right: 12px; + padding-left: 8px; + padding-right: 8px; padding-top: 1px; /* because of -1px top */ line-height: 24px; width: 100%; diff --git a/webapp/packages/plugin-data-spreadsheet-new/package.json b/webapp/packages/plugin-data-spreadsheet-new/package.json index 300f45bbc3..2c1168ddb4 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/package.json +++ b/webapp/packages/plugin-data-spreadsheet-new/package.json @@ -15,7 +15,8 @@ "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix" }, "dependencies": { - "react-data-grid": "^7.0.0-canary.30" + "react-data-grid": "^7.0.0-canary.30", + "react-popper": "^2.2.4" }, "peerDependencies": { "@cloudbeaver/plugin-data-viewer": "^0.1.0", diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx new file mode 100644 index 0000000000..973e49a5d3 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx @@ -0,0 +1,95 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import { useContext, useLayoutEffect, useState } from 'react'; +import type { EditorProps } from 'react-data-grid'; +import { createPortal } from 'react-dom'; +import { usePopper } from 'react-popper'; +import styled, { css } from 'reshadow'; + +import { InlineEditor, InlineEditorControls } from '@cloudbeaver/core-app'; + +import { DataGridContext } from '../DataGridContext'; + +const styles = css` + editor { + composes: theme-typography--body2 from global; + } + InlineEditor { + font-size: 12px; + left: -1px; + } +`; + +export const CellEditor: React.FC> = observer(function CellEditor({ + rowIdx, + row, + column, + editorPortalTarget, + onClose, +}) { + const [elementRef, setElementRef] = useState(null); + const [popperRef, setPopperRef] = useState(null); + const popper = usePopper(elementRef, popperRef, { + placement: 'right', + }); + const context = useContext(DataGridContext); + + useLayoutEffect(() => { + if (elementRef && popperRef) { + const size = elementRef.parentElement?.getBoundingClientRect(); + if (size) { + popperRef.style.width = (size.width + 2) + 'px'; + popperRef.style.height = (size.height + 2) + 'px'; + } + } + }); + + if (!context) { + throw new Error('DataGridContext should be provided'); + } + const modelResultData = context.model?.getResult(context.resultIndex); + + const value = row[column.key]; + + const isLastColumn = column.isLastFrozenColumn; // TODO: do not work + const isLastRow = rowIdx > 0 && ((modelResultData?.data.rows.length || 0) - 1 === rowIdx); + let controlsPosition: InlineEditorControls = 'right'; + + if (isLastColumn) { + controlsPosition = isLastRow ? 'top' : 'bottom'; + } + + const handleSave = () => onClose(false); + const handleReject = () => onClose(false); + const handleChange = () => {}; + const handleUndo = () => onClose(false); + + return styled(styles)( + + {createPortal(( + + + + ), context.getEditorPortal() || editorPortalTarget)} + + ); +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts index d3674173d4..df9101d1cb 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts @@ -10,9 +10,10 @@ import { createContext } from 'react'; import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; -interface IDataGridContext { - model: IDatabaseDataModel; +export interface IDataGridContext { + model: IDatabaseDataModel; resultIndex: number; + getEditorPortal: () => HTMLDivElement | null; } export const DataGridContext = createContext(null); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx index ed65acb4ac..eeedb9f342 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -8,7 +8,7 @@ import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import DataGrid from 'react-data-grid'; import type { Column } from 'react-data-grid'; import styled from 'reshadow'; @@ -21,7 +21,8 @@ import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; import { ResultSetTools } from '../ResultSetTools'; import baseStyles from '../styles/base.scss'; import { reactGridStyles } from '../styles/styles'; -import { DataGridContext } from './DataGridContext'; +import { CellEditor } from './CellEditor/CellEditor'; +import { DataGridContext, IDataGridContext } from './DataGridContext'; import { DataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; import { useGridSelectionContext } from './DataGridSelection/useGridSelectionContext'; import { DataGridSortingContext } from './DataGridSorting/DataGridSortingContext'; @@ -30,7 +31,7 @@ import { DataGridTableContainer } from './DataGridTableContainer'; import { CellFormatter } from './Formatters/CellFormatter'; import { IndexFormatter } from './Formatters/IndexFormatter'; import { RowRenderer } from './RowRenderer/RowRenderer'; -import { TableColumnHeaderNew } from './TableColumnHeader/TableColumnHeader-new'; +import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader'; interface Props { model: IDatabaseDataModel; @@ -54,6 +55,7 @@ const indexColumn: Column = { }; export const DataGridTable: React.FC = observer(function DataGridTable({ model, resultIndex, className }) { + const editorRef = useRef(null); const styles = useStyles(reactGridStyles, baseStyles); const modelResultData = model?.getResult(resultIndex); @@ -114,16 +116,36 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ width: Math.min(300, measuredCells[i]), minWidth: 40, resizable: true, - headerRenderer: TableColumnHeaderNew, + editable: true, + headerRenderer: TableColumnHeader, formatter: CellFormatter, + editor: CellEditor, + editorOptions: { + onCellKeyDown: event => { + event.preventDefault(); + event.stopPropagation(); + }, + onNavigation: event => { + event.preventDefault(); + event.stopPropagation(); + + return false; + }, + }, })); columns.unshift(indexColumn); return { rows, columns }; }), [modelResultData]).get(); + const gridContext = useMemo(() => ({ + model, + resultIndex, + getEditorPortal: () => editorRef.current, + }), [model, resultIndex, editorRef]); + return styled(styles)( - + @@ -136,6 +158,7 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ rowRenderer={RowRenderer} onScroll={handleScroll} /> +
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader-new.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx similarity index 93% rename from webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader-new.tsx rename to webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx index de06e5c8ce..0120c1f6d4 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader-new.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx @@ -6,9 +6,8 @@ * you may not use this file except in compliance with the License. */ -import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useCallback, useContext, useMemo } from 'react'; +import { useContext } from 'react'; import type { HeaderRendererProps } from 'react-data-grid'; import styled, { css, use } from 'reshadow'; @@ -79,7 +78,7 @@ function getColumn(colIdx: number, source: SqlResultSet) { return source.columns?.[colIdx]; } -export const TableColumnHeaderNew: React.FC> = observer(function TableColumnHeaderNew({ +export const TableColumnHeader: React.FC> = observer(function TableColumnHeader({ column: calculatedColumn, }) { const dataGridContext = useContext(DataGridContext); @@ -91,7 +90,7 @@ export const TableColumnHeaderNew: React.FC> = observer const model = dataGridContext.model; const columnName = calculatedColumn.name as string; - const column = getColumn(Number(calculatedColumn.key), model.getResult(dataGridContext.resultIndex).data); + const column = getColumn(Number(calculatedColumn.key), model.getResult(dataGridContext.resultIndex)?.data); const loading = model.isLoading(); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss index c585b8b916..af9b7297bc 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss +++ b/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss @@ -24,4 +24,17 @@ .rdg-cell { border-color: transparent; } + + .rdg-cell-editing { + overflow: visible; + height: 24px; + box-shadow: none !important; + } + + .rdg-editor-container { + position: relative; + display: flex; + width: 100%; + height: 100%; + } } diff --git a/webapp/yarn.lock b/webapp/yarn.lock index fb2980c17d..5dd1f88812 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -11092,11 +11092,24 @@ react-dom@^16.13.1, react-dom@^16.x.x: prop-types "^15.6.2" scheduler "^0.19.1" +react-fast-compare@^3.0.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-popper@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.4.tgz#d2ad3d2474ac9f1abf93df3099d408e5aa6a2e22" + integrity sha512-NacOu4zWupdQjVXq02XpTD3yFPSfg5a7fex0wa3uGKVkFK7UN6LvVxgcb+xYr56UCuWiNPMH20tntdVdJRwYew== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.0.2" + react-sanitized-html@^2.x.x: version "2.0.0" resolved "https://registry.yarnpkg.com/react-sanitized-html/-/react-sanitized-html-2.0.0.tgz#2ac300ae60e8e7cb28378b1233b9630e415d0241" @@ -13350,6 +13363,13 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +warning@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.0.tgz#e63194736bf3aa22026f7b191cd57907b0f9f696"