From a219593e1dbeb769bf596db0de59d3111d89c2e8 Mon Sep 17 00:00:00 2001 From: Ainur <59531286+yagudin10@users.noreply.github.com> Date: Wed, 17 May 2023 12:42:55 +0200 Subject: [PATCH] CB-3303-edit-driver-settings (#1607) * CB-3305 api for driver editing * CB-3305 gql api for driver provider list * CB-3303 feat: file uploading API * CB-3303 feat: support gql request in file upload API * CB-3303 code style fix * CB-3376 file uploading mechanism * CB-3303 feat: gql sdk extension API * CB-3303 feat: gql sdk extensions API rename * CB-3303 add driver mutations, update drivers resource * CB-3303 move driver gql functions to another service * CB-3305 web driver config fix * CB-3305 driver config use same fields as in driver info * CB-3437 add driver service to the codegen.yml * CB-3437 add driver providers * CB-3376 delete used driver library after restart server * CB-3376 driver library add fix * CB-3306 include driver libraries * CB-3303 return empty driver properties array if there is the error * CB-3303 fix conflicts * CB-3437 reuse defaults * CB-3303 feat: add sub-secondary color * CB-3303 fix: use sub-secondary color in tables * CB-3305 delete driver libraries fix * CB-3376 driver libraries save delete fix * CB-3437 naming * CB-3376 do not delete jar library file from the server * CB-3376 create file controller fix * CB-3305 move drivers api to ee * CB-3305 move drivers api to ee * CB-3376 file controller use same path * CB-3305 move driver provider list to ee * CB-3303 move mutations to the EE --------- Co-authored-by: Aleksey Potsetsuev Co-authored-by: Tatiana Fokina Co-authored-by: naumov Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com> Co-authored-by: Alexander Skoblikov --- .../model/app/BaseWebApplication.java | 7 + .../cloudbeaver/model/app/WebApplication.java | 3 + .../cloudbeaver/model/session/WebSession.java | 5 + .../model/session/WebUserContext.java | 8 + .../schema/service.core.graphqls | 9 +- ...Config.java => WebDatabaseDriverInfo.java} | 21 +- .../model/WebDriverLibraryInfo.java | 50 +++++ .../model/user/WebDataSourceProviderInfo.java | 47 ++++ .../cloudbeaver/server/CBApplicationCE.java | 10 + .../src/io/cloudbeaver/server/CBPlatform.java | 10 +- .../service/core/DBWServiceCore.java | 10 +- .../service/core/WebServiceBindingCore.java | 1 - .../service/core/impl/WebServiceCore.java | 7 +- .../src/FormControls/FormContext.ts | 7 +- ...InputFile.tsx => InputFileTextContent.tsx} | 6 +- .../src/FormControls/InputFiles.tsx | 206 ++++++++++++++++++ .../src/FormControls/SubmittingForm.tsx | 4 +- .../ObjectPropertyInfoForm/RenderField.tsx | 6 +- .../src/Table/BASE_TABLE_STYLES.ts | 2 +- webapp/packages/core-blocks/src/Tags/Tag.tsx | 20 +- .../packages/core-blocks/src/UploadArea.tsx | 30 ++- webapp/packages/core-blocks/src/index.ts | 3 +- .../packages/core-blocks/src/useRefInherit.ts | 32 +++ .../core-connections/src/DBDriverResource.ts | 59 ++++- webapp/packages/core-connections/src/index.ts | 1 + .../core-localization/src/locales/en.ts | 1 + .../core-localization/src/locales/it.ts | 1 + .../core-localization/src/locales/ru.ts | 1 + .../core-localization/src/locales/zh.ts | 1 + webapp/packages/core-sdk/package.json | 3 +- .../core-sdk/src/CustomGraphQLClient.ts | 102 +++++++++ .../uploadDriverLibraryExtension.ts | 38 ++++ .../packages/core-sdk/src/GraphQLService.ts | 14 +- .../core-sdk/src/Resource/CachedResource.ts | 16 +- .../src/queries/connections/driverList.gql | 2 +- .../src/queries/fragments/DatabaseDriver.gql | 9 + webapp/packages/core-sdk/src/sdk.ts | 22 +- .../core-theming/src/styles/_theme-dark.scss | 6 + .../core-theming/src/styles/_theme-light.scss | 6 + .../core-theming/src/styles/_variables.scss | 2 + .../ServerConfigurationDriversForm.tsx | 2 - .../src/styles/themes/_base-react-grid.scss | 2 +- .../styles/themes/_base-metadata-grid.scss | 2 +- webapp/yarn.lock | 9 + 44 files changed, 738 insertions(+), 65 deletions(-) rename server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/{WebDatabaseDriverConfig.java => WebDatabaseDriverInfo.java} (91%) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebDataSourceProviderInfo.java rename webapp/packages/core-blocks/src/FormControls/{InputFile.tsx => InputFileTextContent.tsx} (95%) create mode 100644 webapp/packages/core-blocks/src/FormControls/InputFiles.tsx create mode 100644 webapp/packages/core-blocks/src/useRefInherit.ts create mode 100644 webapp/packages/core-sdk/src/Extensions/uploadDriverLibraryExtension.ts diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java index ad3544705f..61090e8d5c 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java @@ -28,6 +28,7 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBFileController; import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; @@ -73,6 +74,12 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements throw new IllegalStateException("Resource controller is not supported by " + getClass().getSimpleName()); } + @NotNull + @Override + public DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider) { + throw new IllegalStateException("File controller is not supported by " + getClass().getSimpleName()); + } + @Nullable @Override public Path getDefaultWorkingFolder() { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java index 4be9903b00..319f5debbb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java @@ -21,6 +21,7 @@ import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBFileController; import org.jkiss.dbeaver.model.app.DBPApplication; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; @@ -63,6 +64,8 @@ public interface WebApplication extends DBPApplication { RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider, @NotNull DBPWorkspace workspace) throws DBException; + DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider); + String getServerURL(); default String getServicesURI() { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index f45dadfc3a..a75154fd9c 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -40,6 +40,7 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBFileController; import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.access.DBAAuthCredentials; import org.jkiss.dbeaver.model.access.DBACredentialsProvider; @@ -225,6 +226,10 @@ public class WebSession extends BaseWebSession return userContext.getRmController(); } + public synchronized DBFileController getFileController() { + return userContext.getFileController(); + } + public synchronized void refreshUserData() { refreshSessionAuth(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java index aa72ace32f..b118e206f2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java @@ -23,6 +23,7 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBFileController; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.auth.SMAuthStatus; @@ -63,6 +64,7 @@ public class WebUserContext implements SMCredentialsProvider { private SMAdminController adminSecurityController; private DBSSecretController secretController; private RMController rmController; + private DBFileController fileController; private Set accessibleProjectIds = new HashSet<>(); public WebUserContext(WebApplication application, DBPWorkspace workspace) throws DBException { @@ -70,6 +72,7 @@ public class WebUserContext implements SMCredentialsProvider { this.workspace = workspace; this.securityController = application.createSecurityController(this); this.rmController = application.createResourceController(this, workspace); + this.fileController = application.createFileController(this); setUserPermissions(getDefaultPermissions()); } @@ -111,6 +114,7 @@ public class WebUserContext implements SMCredentialsProvider { this.adminSecurityController = application.getAdminSecurityController(this); this.secretController = application.getSecretController(this); this.rmController = application.createResourceController(this, workspace); + this.fileController = application.createFileController(this); if (isSessionChanged) { this.smSessionId = smAuthPermissions.getSessionId(); setUser(smAuthPermissions.getUserId() == null ? null : new WebUser(securityController.getCurrentUser())); @@ -226,6 +230,10 @@ public class WebUserContext implements SMCredentialsProvider { return rmController; } + public DBFileController getFileController() { + return fileController; + } + @NotNull public Set getAccessibleProjectIds() { return accessibleProjectIds; diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index f42835760b..b770bda88d 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -235,6 +235,14 @@ type DriverInfo { applicableNetworkHandlers: [ID]! configurationTypes: [DriverConfigurationType]! + + driverLibraries: [DriverLibraryInfo!]! +} + +type DriverLibraryInfo { + id: ID! + name: String! + icon: String! } enum ResultDataFormat { @@ -574,6 +582,5 @@ extend type Mutation { asyncTaskCancel(id: String!): Boolean asyncTaskInfo(id: String!, removeOnFinish: Boolean!): AsyncTaskInfo! - } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java similarity index 91% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverConfig.java rename to server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java index 046e14cc09..dd6b7de656 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java @@ -21,11 +21,9 @@ import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.ConfigurationUtils; import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBConstants; -import org.jkiss.dbeaver.model.connection.DBPAuthModelDescriptor; -import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; -import org.jkiss.dbeaver.model.connection.DBPDriver; -import org.jkiss.dbeaver.model.connection.DBPDriverConfigurationType; +import org.jkiss.dbeaver.model.connection.*; import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNative; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; @@ -41,15 +39,16 @@ import java.util.Map; /** * Web driver configuration */ -public class WebDatabaseDriverConfig { +public class WebDatabaseDriverInfo { + private static final Log log = Log.getLog(WebDatabaseDriverInfo.class); public static final String URL_SERVER_FIELD = "{server}"; public static final String URL_DATABASE_FIELD = ".*(?:\\{(?:database|file|folder)}).*"; private final WebSession webSession; private final DBPDriver driver; private String id; - public WebDatabaseDriverConfig(WebSession webSession, DBPDriver driver) { + public WebDatabaseDriverInfo(WebSession webSession, DBPDriver driver) { this.webSession = webSession; this.driver = driver; this.id = driver.getFullId(); @@ -191,7 +190,8 @@ public class WebDatabaseDriverConfig { return Arrays.stream(properties) .map(p -> new WebPropertyInfo(webSession, p, propertySource)).toArray(WebPropertyInfo[]::new); } catch (DBException e) { - throw new DBWebException("Error reading driver properties", e); + log.error("Error reading driver properties", e); + return new WebPropertyInfo[0]; } } @@ -259,4 +259,11 @@ public class WebDatabaseDriverConfig { } return driver.getSampleURL().matches(URL_DATABASE_FIELD); } + + @Property + public WebDriverLibraryInfo[] getDriverLibraries() { + return driver.getDriverLibraries().stream() + .map(dbpDriverLibrary -> new WebDriverLibraryInfo(webSession, dbpDriverLibrary)) + .toArray(WebDriverLibraryInfo[]::new); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java new file mode 100644 index 0000000000..3ce4be899a --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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; + +import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; +import org.jkiss.dbeaver.model.meta.Property; + +public class WebDriverLibraryInfo { + + private final WebSession webSession; + private final DBPDriverLibrary driverLibrary; + + public WebDriverLibraryInfo(@NotNull WebSession webSession, @NotNull DBPDriverLibrary driverLibrary) { + this.webSession = webSession; + this.driverLibrary = driverLibrary; + } + + + @Property + public String getId() { + return driverLibrary.getId(); + } + + @Property + public String getName() { + return driverLibrary.getDisplayName(); + } + + @Property + public String getIcon() { + return WebServiceUtils.makeIconId(driverLibrary.getIcon()); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebDataSourceProviderInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebDataSourceProviderInfo.java new file mode 100644 index 0000000000..a749c75e70 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebDataSourceProviderInfo.java @@ -0,0 +1,47 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.user; + +import io.cloudbeaver.WebServiceUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor; +import org.jkiss.dbeaver.model.meta.Property; + +public class WebDataSourceProviderInfo { + + @NotNull + private final DBPDataSourceProviderDescriptor providerDescriptor; + + public WebDataSourceProviderInfo(@NotNull DBPDataSourceProviderDescriptor descriptor) { + this.providerDescriptor = descriptor; + } + + @Property + public String getId() { + return providerDescriptor.getId(); + } + + @Property + public String getName() { + return providerDescriptor.getName(); + } + + @Property + public String getIcon() { + return WebServiceUtils.makeIconId(providerDescriptor.getIcon()); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java index d934e2c512..456970f70b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java @@ -25,11 +25,15 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBFileController; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMController; +import org.jkiss.dbeaver.registry.BasePlatformImpl; +import org.jkiss.dbeaver.registry.LocalFileController; +import org.jkiss.dbeaver.runtime.DBWorkbench; import java.util.List; @@ -70,6 +74,12 @@ public class CBApplicationCE extends CBApplication { return LocalResourceController.builder(credentialsProvider, workspace, this::getSecurityController).build(); } + @NotNull + @Override + public DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider) { + return new LocalFileController(DBWorkbench.getPlatform().getWorkspace().getAbsolutePath().resolve(DBFileController.DATA_FOLDER)); + } + protected void shutdown() { try { if (securityController instanceof CBEmbeddedSecurityController) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java index 5797a7e871..1ca113cc1f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java @@ -17,6 +17,7 @@ package io.cloudbeaver.server; +import io.cloudbeaver.auth.NoAuthCredentialsProvider; import io.cloudbeaver.server.jobs.SessionStateJob; import io.cloudbeaver.server.jobs.WebSessionMonitorJob; import io.cloudbeaver.service.session.WebSessionManager; @@ -27,6 +28,7 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBFileController; import org.jkiss.dbeaver.model.app.DBACertificateStorage; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor; @@ -41,6 +43,7 @@ import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; import org.jkiss.dbeaver.registry.BasePlatformImpl; import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; +import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.runtime.SecurityProviderUtils; import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter; import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl; @@ -289,7 +292,7 @@ public class CBPlatform extends BasePlatformImpl { } boolean hasAllFiles = true, hasJars = false; for (DBPDriverLibrary lib : libraries) { - if (!lib.isOptional() && lib.getType() != DBPDriverLibrary.FileType.license && + if (!DBWorkbench.isDistributed() && !lib.isOptional() && lib.getType() != DBPDriverLibrary.FileType.license && (lib.getLocalFile() == null || !Files.exists(lib.getLocalFile()))) { hasAllFiles = false; @@ -309,4 +312,9 @@ public class CBPlatform extends BasePlatformImpl { log.info("Available drivers: " + applicableDrivers.stream().map(DBPDriver::getFullName).collect(Collectors.joining(","))); } + @NotNull + @Override + public DBFileController createFileController() { + return getApplication().createFileController(new NoAuthCredentialsProvider()); + } } 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 f4ceeaefa1..10a116b558 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 @@ -16,12 +16,10 @@ */ package io.cloudbeaver.service.core; -import io.cloudbeaver.DBWebException; -import io.cloudbeaver.WebAction; -import io.cloudbeaver.WebObjectId; -import io.cloudbeaver.WebProjectAction; +import io.cloudbeaver.*; import io.cloudbeaver.model.*; import io.cloudbeaver.model.session.WebSession; + import io.cloudbeaver.model.user.WebDataSourceProviderInfo; import io.cloudbeaver.service.DBWService; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -30,6 +28,8 @@ import org.jkiss.dbeaver.model.rm.RMConstants; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -42,7 +42,7 @@ public interface DBWServiceCore extends DBWService { WebServerConfig getServerConfig() throws DBWebException; @WebAction - List getDriverList(@NotNull WebSession webSession, String driverId) throws DBWebException; + List getDriverList(@NotNull WebSession webSession, String driverId) throws DBWebException; @WebAction List getAuthModels(@NotNull WebSession webSession); 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 c7cf19afaf..1347234a8a 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 @@ -184,5 +184,4 @@ public class WebServiceBindingCore extends WebServiceBindingBase private WebConnectionConfig getConnectionConfig(DataFetchingEnvironment env) { return new WebConnectionConfig(env.getArgument("config")); } - } 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 10a0eacfa8..60e7afab13 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 @@ -23,6 +23,7 @@ import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.*; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.user.WebDataSourceProviderInfo; import io.cloudbeaver.registry.WebHandlerRegistry; import io.cloudbeaver.registry.WebSessionHandlerDescriptor; import io.cloudbeaver.server.CBApplication; @@ -81,11 +82,11 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public List getDriverList(@NotNull WebSession webSession, String driverId) { - List result = new ArrayList<>(); + public List getDriverList(@NotNull WebSession webSession, String driverId) { + List result = new ArrayList<>(); for (DBPDriver driver : CBPlatform.getInstance().getApplicableDrivers()) { if (driverId == null || driverId.equals(driver.getFullId())) { - result.add(new WebDatabaseDriverConfig(webSession, driver)); + result.add(new WebDatabaseDriverInfo(webSession, driver)); } } return result; diff --git a/webapp/packages/core-blocks/src/FormControls/FormContext.ts b/webapp/packages/core-blocks/src/FormControls/FormContext.ts index 6402a381d5..03be5053c1 100644 --- a/webapp/packages/core-blocks/src/FormControls/FormContext.ts +++ b/webapp/packages/core-blocks/src/FormControls/FormContext.ts @@ -10,16 +10,17 @@ import { createContext } from 'react'; import type { IExecutor } from '@cloudbeaver/core-executor'; -type ChangeHandler = (value: string | number | boolean | null | undefined, name: string | undefined) => void; +export type FormChangeValues = string | number | boolean | FileList | null | undefined; +export type FormChangeHandler = (value: FormChangeValues, name: string | undefined) => void; export interface IChangeData { - value: string | number | boolean | null | undefined; + value: FormChangeValues; name: string | undefined; } export interface IFormContext { changeExecutor: IExecutor; - change: ChangeHandler; + change: FormChangeHandler; } export const FormContext = createContext(null); diff --git a/webapp/packages/core-blocks/src/FormControls/InputFile.tsx b/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx similarity index 95% rename from webapp/packages/core-blocks/src/FormControls/InputFile.tsx rename to webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx index 8002a060c4..e5e492e4f8 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputFile.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputFileTextContent.tsx @@ -67,9 +67,11 @@ interface Props extends ILayoutSizeProps { mapValue?: (value: string) => string; } -type InputFileType = >(props: Props) => React.ReactElement; +type InputFileTextContentType = >( + props: Props +) => React.ReactElement; -export const InputFile: InputFileType = observer(function InputFile({ +export const InputFileTextContent: InputFileTextContentType = observer(function InputFileTextContent({ name, state, accept, diff --git a/webapp/packages/core-blocks/src/FormControls/InputFiles.tsx b/webapp/packages/core-blocks/src/FormControls/InputFiles.tsx new file mode 100644 index 0000000000..34f1038e6e --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/InputFiles.tsx @@ -0,0 +1,206 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { forwardRef, useContext, useEffect, useState } from 'react'; +import styled, { use, css } from 'reshadow'; + +import type { ComponentStyle } from '@cloudbeaver/core-theming'; + +import { Button } from '../Button'; +import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; +import { useTranslate } from '../localization/useTranslate'; +import { Tag } from '../Tags/Tag'; +import { Tags } from '../Tags/Tags'; +import { UploadArea } from '../UploadArea'; +import { useCombinedHandler } from '../useCombinedHandler'; +import { useRefInherit } from '../useRefInherit'; +import { useStateDelay } from '../useStateDelay'; +import { useStyles } from '../useStyles'; +import { baseFormControlStyles, baseInvalidFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles'; +import { FormContext } from './FormContext'; +import { isControlPresented } from './isControlPresented'; + +const INPUT_FIELD_STYLES = css` + field-label { + display: block; + composes: theme-typography--body1 from global; + font-weight: 500; + } + field-label:not(:empty) { + padding-bottom: 10px; + } + input-container { + position: relative; + } + Tags { + padding-top: 8px; + + &:empty { + display: none; + } + } +`; + +type BaseProps = Omit, 'onChange' | 'name' | 'value' | 'style'> & ILayoutSizeProps & { + error?: boolean; + loading?: boolean; + description?: string; + labelTooltip?: string; + hideTags?: boolean; + mod?: 'surface'; + ref?: React.Ref; + style?: ComponentStyle; +}; + +type ControlledProps = BaseProps & { + name?: string; + value?: FileList | null; + onChange?: (value: FileList | null, name?: string) => any; + state?: never; + autoHide?: never; +}; + +type ObjectProps = BaseProps & { + name: TKey; + state: TState; + onChange?: (value: TState[TKey], name: TKey) => any; + autoHide?: boolean; + value?: never; +}; + +interface InputFilesType { + (props: ControlledProps): React.ReactElement | null; + (props: ObjectProps): React.ReactElement | null; +} + +export const InputFiles: InputFilesType = observer(forwardRef(function InputFiles({ + name, + style, + value: valueControlled, + required, + state, + children, + className, + error, + loading, + description, + labelTooltip, + hideTags, + mod, + fill, + small, + medium, + large, + tiny, + autoHide, + onChange, + ...rest +}: ControlledProps | ObjectProps, refInherit: React.Ref) { + const ref = useRefInherit(refInherit); + const [innerState, setInnerState] = useState(null); + const translate = useTranslate(); + const styles = useStyles( + baseFormControlStyles, + error ? baseInvalidFormControlStyles : baseValidFormControlStyles, + INPUT_FIELD_STYLES, + style + ); + const context = useContext(FormContext); + loading = useStateDelay(loading ?? false, 300); + + let value = valueControlled ?? innerState; + + if (state && name !== undefined && name in state) { + value = state[name]; + } + + function setValue(value: FileList | null) { + setInnerState(value); + if (state) { + state[name] = value; + } + if (onChange) { + onChange(value, name); + } + if (context) { + context.change(value, name); + } + } + + const removeFile = useCombinedHandler(function removeFile(index: number): void { + if (!value) { + return; + } + + const dt = new DataTransfer(); + + for (let i = 0; i < value.length; i++) { + const file = value[i]; + if (index !== i) { + dt.items.add(file); + } + } + + setValue(dt.files.length === 0 ? null : dt.files); + }); + + const handleChange = useCombinedHandler((event: React.ChangeEvent) => { + setValue(event.target.files); + }); + + useEffect(() => { + if (value !== innerState) { + setInnerState(value); + } + }); + + if (autoHide && !isControlPresented(name, state)) { + return null; + } + + const files = Array.from(value ?? []); + + return styled(styles)( + + {children}{required && ' *'} + + + + + {!hideTags && ( + + {files.map((file, i) => ( + + ))} + + )} + + {description && ( + + {description} + + )} + + ); +})); diff --git a/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx b/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx index b821426ff3..1abb819524 100644 --- a/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx +++ b/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx @@ -11,7 +11,7 @@ import React, { forwardRef, useContext, useState } from 'react'; import { Executor } from '@cloudbeaver/core-executor'; import { useObjectRef } from '../useObjectRef'; -import { FormContext, IChangeData, IFormContext } from './FormContext'; +import { FormChangeHandler, FormContext, IChangeData, IFormContext } from './FormContext'; export interface IFormStateInfo { edited: boolean; @@ -24,7 +24,7 @@ type FormDetailedProps = Omit) => Promise | void; - onChange?: (value: string | number | boolean | null | undefined, name: string | undefined) => void; + onChange?: FormChangeHandler; }; export const SubmittingForm = forwardRef(function SubmittingForm( diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx index 9b9a463cbd..ef5ce07373 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx @@ -15,7 +15,7 @@ import { FieldCheckbox } from '../../FormControls/Checkboxes/FieldCheckbox'; import { Combobox } from '../../FormControls/Combobox'; import { FormFieldDescription } from '../../FormControls/FormFieldDescription'; import { InputField } from '../../FormControls/InputField'; -import { InputFile } from '../../FormControls/InputFile'; +import { InputFileTextContent } from '../../FormControls/InputFileTextContent'; import { isControlPresented } from '../../FormControls/isControlPresented'; import { Textarea } from '../../FormControls/Textarea'; import { Link } from '../../Link'; @@ -94,7 +94,7 @@ export const RenderField = observer(function RenderField({ if (controltype === 'file' && state) { return ( - (function RenderField({ mapValue={removeMetadataFromBase64} > {property.displayName} - + ); } diff --git a/webapp/packages/core-blocks/src/Table/BASE_TABLE_STYLES.ts b/webapp/packages/core-blocks/src/Table/BASE_TABLE_STYLES.ts index d174de1b8b..e34ceb9b45 100644 --- a/webapp/packages/core-blocks/src/Table/BASE_TABLE_STYLES.ts +++ b/webapp/packages/core-blocks/src/Table/BASE_TABLE_STYLES.ts @@ -73,7 +73,7 @@ tbody > tr { &:not([|noHover]):hover, &:not([|noHover])[|selected], &:not([|noHover])[|expanded] { - background-color: var(--theme-secondary); + background-color: var(--theme-sub-secondary); } &[|disabled] { diff --git a/webapp/packages/core-blocks/src/Tags/Tag.tsx b/webapp/packages/core-blocks/src/Tags/Tag.tsx index 09af2c0335..c51a34c94a 100644 --- a/webapp/packages/core-blocks/src/Tags/Tag.tsx +++ b/webapp/packages/core-blocks/src/Tags/Tag.tsx @@ -11,8 +11,8 @@ import styled, { css } from 'reshadow'; import { IconOrImage } from '../IconOrImage'; -export interface ITag { - id: string; +export interface ITag { + id: T; label: string; icon?: string; } @@ -53,8 +53,10 @@ const style = css` padding: 4px; cursor: pointer; flex-shrink: 0; + opacity: 0.5; + &:hover { - opacity: 0.7; + opacity: 1; } } IconOrImage { @@ -63,12 +65,18 @@ const style = css` } `; -interface Props extends ITag { - onRemove: (id: string) => void; +interface Props extends ITag { + onRemove: (id: T) => void; className?: string; } -export const Tag = observer(function Tag({ id, label, icon, onRemove, className }) { +export const Tag = observer(function Tag({ + id, + label, + icon, + onRemove, + className, +}: Props) { return styled(style)( {icon && ( diff --git a/webapp/packages/core-blocks/src/UploadArea.tsx b/webapp/packages/core-blocks/src/UploadArea.tsx index 6baf75d209..897580beeb 100644 --- a/webapp/packages/core-blocks/src/UploadArea.tsx +++ b/webapp/packages/core-blocks/src/UploadArea.tsx @@ -6,11 +6,15 @@ * you may not use this file except in compliance with the License. */ +import { forwardRef, useLayoutEffect } from 'react'; import styled, { css, use } from 'reshadow'; import { uuid } from '@cloudbeaver/core-utils'; -interface Props extends React.DetailedHTMLProps, HTMLInputElement> { +import { useRefInherit } from './useRefInherit'; + +interface Props extends Omit, HTMLInputElement>, 'value'> { + value?: FileList | null; className?: string; /** Reset prev file path. This ensures that the onChange event will be triggered for the same file as well */ reset?: boolean; @@ -19,6 +23,7 @@ interface Props extends React.DetailedHTMLProps = function UploadArea({ id, reset, className, children, ...rest }) { - const _id = id ?? uuid(); +export const UploadArea = forwardRef(function UploadArea({ + id = uuid(), + value, + reset, + children, + className, + ...rest +}, refInherit) { + const ref = useRefInherit(refInherit); const handleChange = async (event: React.ChangeEvent) => { if (rest.onChange) { @@ -40,12 +52,18 @@ export const UploadArea: React.FC = function UploadArea({ id, reset, clas } }; + useLayoutEffect(() => { + if (ref.current && value !== undefined) { + ref.current.files = value; + } + }); + return styled(styles)( <> - -