diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java index b57e8ed021..b4a22bb6a0 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java @@ -448,5 +448,12 @@ public int getKeepAliveInterval() { return dataSourceContainer.getConnectionConfiguration().getKeepAliveInterval(); } + @Property + public List getSharedSecrets() throws DBException { + return dataSourceContainer.listSharedCredentials() + .stream() + .map(WebSecretInfo::new) + .collect(Collectors.toList()); + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebSecretInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebSecretInfo.java new file mode 100644 index 0000000000..ee412bde07 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebSecretInfo.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 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 org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; + +public class WebSecretInfo { + private final DBSSecretValue secretValue; + + public WebSecretInfo(DBSSecretValue secretValue) { + this.secretValue = secretValue; + } + + @Property + public String getDisplayName() { + return secretValue.getDisplayName(); + } + + @Property + public String getSecretId() { + return secretValue.getSubjectId(); + } +} 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 d975fbe207..4f2a01b5a5 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 @@ -1008,6 +1008,17 @@ public WebProjectImpl getProjectById(@Nullable String projectId) { return getWorkspace().getProjectById(projectId); } + public WebProjectImpl getAccessibleProjectById(@Nullable String projectId) throws DBWebException { + WebProjectImpl project = null; + if (projectId != null) { + project = getWorkspace().getProjectById(projectId); + } + if (project == null) { + throw new DBWebException("Project not found: " + projectId); + } + return project; + } + public List getAccessibleProjects() { return getWorkspace().getProjects(); } diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 0b2124d5e9..e0b7e6ae7b 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -308,6 +308,11 @@ type NetworkHandlerConfig { secureProperties: Object! } +type SecretInfo { + displayName: String! + secretId: String! +} + # Connection instance type ConnectionInfo { id: ID! @@ -337,6 +342,8 @@ type ConnectionInfo { saveCredentials: Boolean! # Shared credentials - the same for all users, stored in secure storage. sharedCredentials: Boolean! + + sharedSecrets: [SecretInfo!]! @since(version: "23.3.5") # Determines that credentials were saved for current user. # This field read is slow, it should be read only when it really needed credentialsSaved: Boolean! @@ -488,6 +495,7 @@ input ConnectionConfig { saveCredentials: Boolean sharedCredentials: Boolean authModelId: ID + selectedSecretId: ID @since(version: "23.3.5") credentials: Object # Map of provider properties (name/value) @@ -584,14 +592,14 @@ extend type Mutation { copyConnectionFromNode( nodePath: String!, config: ConnectionConfig, projectId: ID ): ConnectionInfo! # Test connection configuration. Returns remote server version - testConnection( config: ConnectionConfig!, projectId: ID ): ConnectionInfo! + testConnection( config: ConnectionConfig!, projectId: ID): ConnectionInfo! # Test connection configuration. Returns remote server version testNetworkHandler( config: NetworkHandlerConfigInput! ): NetworkEndpointInfo! # Initiate existing connection initConnection( id: ID!, projectId: ID, credentials: Object, networkCredentials: [NetworkHandlerConfigInput!], - saveCredentials:Boolean, sharedCredentials: Boolean ): ConnectionInfo! + saveCredentials:Boolean, sharedCredentials: Boolean, selectedSecretId:String ): ConnectionInfo! # Disconnect from database closeConnection( id: ID!, projectId: ID ): ConnectionInfo! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java index ade7f8c719..758ece7742 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java @@ -16,6 +16,7 @@ */ package io.cloudbeaver.model; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.connection.DBPDriverConfigurationType; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.meta.Property; @@ -59,6 +60,7 @@ public class WebConnectionConfig { private Map providerProperties; private List networkHandlersConfig; private DBPDriverConfigurationType configurationType; + private String selectedSecretId; public WebConnectionConfig() { } @@ -91,6 +93,7 @@ public WebConnectionConfig(Map params) { properties = JSONUtils.getObjectOrNull(params, "properties"); userName = JSONUtils.getString(params, "userName"); userPassword = JSONUtils.getString(params, "userPassword"); + selectedSecretId = JSONUtils.getString(params, "selectedSecretId"); authModelId = JSONUtils.getString(params, "authModelId"); credentials = JSONUtils.getObjectOrNull(params, "credentials"); @@ -231,4 +234,9 @@ public Map getProviderProperties() { public Integer getKeepAliveInterval() { return keepAliveInterval; } + + @Nullable + public String getSelectedSecretId() { + return selectedSecretId; + } } 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 ab620fc175..65007f34b9 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 @@ -113,7 +113,8 @@ WebConnectionInfo initConnection( @NotNull Map authProperties, @Nullable List networkCredentials, @Nullable Boolean saveCredentials, - @Nullable Boolean sharedCredentials + @Nullable Boolean sharedCredentials, + @Nullable String selectedCredentials ) throws DBWebException; @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) 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 80a416f261..76f2e718cd 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 @@ -132,7 +132,8 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("credentials"), nhc, env.getArgument("saveCredentials"), - env.getArgument("sharedCredentials") + env.getArgument("sharedCredentials"), + env.getArgument("selectedSecretId") ); } ) 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 2e60d03d4a..e814113d04 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 @@ -54,6 +54,7 @@ import org.jkiss.dbeaver.model.rm.RMProjectType; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; import org.jkiss.dbeaver.registry.DataSourceDescriptor; @@ -318,15 +319,32 @@ public WebConnectionInfo initConnection( @NotNull Map authProperties, @Nullable List networkCredentials, @Nullable Boolean saveCredentials, - @Nullable Boolean sharedCredentials + @Nullable Boolean sharedCredentials, + @Nullable String selectedSecretId ) throws DBWebException { WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId); connectionInfo.setSavedCredentials(authProperties, networkCredentials); - DBPDataSourceContainer dataSourceContainer = connectionInfo.getDataSourceContainer(); + var dataSourceContainer = (DataSourceDescriptor) connectionInfo.getDataSourceContainer(); if (dataSourceContainer.isConnected()) { throw new DBWebException("Datasource '" + dataSourceContainer.getName() + "' is already connected"); } + if (dataSourceContainer.isSharedCredentials() && selectedSecretId != null) { + List allSecrets; + try { + allSecrets = dataSourceContainer.listSharedCredentials(); + } catch (DBException e) { + throw new DBWebException("Error loading connection secret", e); + } + DBSSecretValue selectedSecret = + allSecrets.stream() + .filter(secret -> selectedSecretId.equals(secret.getSubjectId())) + .findFirst().orElse(null); + if (selectedSecret == null) { + throw new DBWebException("Secret not found:" + selectedSecretId); + } + dataSourceContainer.setSelectedSharedCredentials(selectedSecret); + } boolean oldSavePassword = dataSourceContainer.isSavePassword(); try { @@ -642,12 +660,12 @@ public WebConnectionInfo testConnection( connectionConfig.setSaveCredentials(true); // It is used in createConnectionFromConfig - DBPDataSourceContainer dataSource = WebDataSourceUtils.getLocalOrGlobalDataSource( + DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource( CBApplication.getInstance(), webSession, projectId, connectionId); WebProjectImpl project = getProjectById(webSession, projectId); DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); - DBPDataSourceContainer testDataSource; + DataSourceDescriptor testDataSource; if (dataSource != null) { try { // Check that creds are saved to trigger secrets resolve @@ -656,12 +674,27 @@ public WebConnectionInfo testConnection( throw new DBWebException("Can't determine whether datasource credentials are saved", e); } - testDataSource = dataSource.createCopy(dataSource.getRegistry()); + testDataSource = (DataSourceDescriptor) dataSource.createCopy(dataSource.getRegistry()); WebServiceUtils.setConnectionConfiguration( testDataSource.getDriver(), testDataSource.getConnectionConfiguration(), connectionConfig ); + if (connectionConfig.getSelectedSecretId() != null) { + try { + DBSSecretValue secretValue = dataSource.listSharedCredentials() + .stream() + .filter(secret -> connectionConfig.getSelectedSecretId().equals(secret.getSubjectId())) + .findFirst() + .orElse(null); + + if (secretValue != null) { + testDataSource.setSelectedSharedCredentials(secretValue); + } + } catch (DBException e) { + throw new DBWebException("Failed to load secret value: " + connectionConfig.getSelectedSecretId()); + } + } WebServiceUtils.saveAuthProperties( testDataSource, testDataSource.getConnectionConfiguration(), @@ -671,7 +704,8 @@ public WebConnectionInfo testConnection( true ); } else { - testDataSource = WebServiceUtils.createConnectionFromConfig(connectionConfig, sessionRegistry); + testDataSource = (DataSourceDescriptor) WebServiceUtils.createConnectionFromConfig(connectionConfig, + sessionRegistry); } webSession.provideAuthParameters(webSession.getProgressMonitor(), testDataSource, testDataSource.getConnectionConfiguration()); testDataSource.setSavePassword(true); // We need for test to avoid password callback diff --git a/webapp/packages/core-authentication/src/AppAuthService.ts b/webapp/packages/core-authentication/src/AppAuthService.ts index 144ce9e331..bf2066c28a 100644 --- a/webapp/packages/core-authentication/src/AppAuthService.ts +++ b/webapp/packages/core-authentication/src/AppAuthService.ts @@ -23,8 +23,8 @@ export class AppAuthService extends Bootstrap { get loaders(): ILoadableState[] { return [ - getCachedDataResourceLoaderState(this.userInfoResource, undefined), - getCachedDataResourceLoaderState(this.serverConfigResource, undefined), + getCachedDataResourceLoaderState(this.userInfoResource, () => undefined), + getCachedDataResourceLoaderState(this.serverConfigResource, () => undefined), ]; } diff --git a/webapp/packages/core-blocks/public/icons/success_sm.svg b/webapp/packages/core-blocks/public/icons/success_sm.svg index 818bebf74f..ed6811d778 100644 --- a/webapp/packages/core-blocks/public/icons/success_sm.svg +++ b/webapp/packages/core-blocks/public/icons/success_sm.svg @@ -1,3 +1,3 @@ - + diff --git a/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx b/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx index ef394cb054..0004d41f46 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx @@ -16,7 +16,7 @@ import { Button } from '../Button'; import { Container } from '../Containers/Container'; import { Fill } from '../Fill'; import { Form } from '../FormControls/Form'; -import { InputField } from '../FormControls/InputField'; +import { InputField } from '../FormControls/InputField/InputField'; import { useTranslate } from '../localization/useTranslate'; import { s } from '../s'; import { useFocus } from '../useFocus'; diff --git a/webapp/packages/core-blocks/src/Containers/Group.m.css b/webapp/packages/core-blocks/src/Containers/Group.m.css index dae0c832fc..c2f128d261 100644 --- a/webapp/packages/core-blocks/src/Containers/Group.m.css +++ b/webapp/packages/core-blocks/src/Containers/Group.m.css @@ -1,6 +1,18 @@ -.group { +/* + * 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. + */ + +.secondary { + composes: theme-background-secondary theme-text-on-secondary from global; +} +.surface { composes: theme-background-surface theme-text-on-surface from global; - +} +.group { align-content: baseline; box-sizing: border-box; padding: 24px; diff --git a/webapp/packages/core-blocks/src/Containers/Group.tsx b/webapp/packages/core-blocks/src/Containers/Group.tsx index 3bcdca638b..1f33020ae2 100644 --- a/webapp/packages/core-blocks/src/Containers/Group.tsx +++ b/webapp/packages/core-blocks/src/Containers/Group.tsx @@ -17,13 +17,14 @@ import elementsSizeStyles from './shared/ElementsSize.m.css'; interface Props extends IContainerProps { form?: boolean; + secondary?: boolean; center?: boolean; box?: boolean; boxNoOverflow?: boolean; } export const Group = forwardRef>(function Group( - { form, center, box, boxNoOverflow, className, ...rest }, + { form, center, box, secondary, boxNoOverflow, className, ...rest }, ref, ) { const styles = useS(style, containerStyles, elementsSizeStyles); @@ -40,6 +41,8 @@ export const Group = forwardRef .fill { + flex-grow: 1; max-width: none; } diff --git a/webapp/packages/core-blocks/src/ExceptionMessage.tsx b/webapp/packages/core-blocks/src/ExceptionMessage.tsx index 77de07eb02..0a4ebdb3f8 100644 --- a/webapp/packages/core-blocks/src/ExceptionMessage.tsx +++ b/webapp/packages/core-blocks/src/ExceptionMessage.tsx @@ -17,7 +17,7 @@ import { useErrorDetails } from './useErrorDetails'; import { useS } from './useS'; interface Props { - exception?: Error; + exception?: Error | null; icon?: boolean; inline?: boolean; className?: string; @@ -39,6 +39,10 @@ export const ExceptionMessage = observer(function ExceptionMessage({ exce }; } + if (!exception) { + return null; + } + return (
@@ -51,7 +55,8 @@ export const ExceptionMessage = observer(function ExceptionMessage({ exce {translate('core_blocks_exception_message_error_title')}
- {translate('core_blocks_exception_message_error_message')} {onRetry && translate('ui_please_retry')} + {(error.hasDetails && error.message) || translate('core_blocks_exception_message_error_message')}{' '} + {onRetry && translate('ui_please_retry')}
+ {!disableTest && ( + + )} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts index e886f75a31..86c45f8abd 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts @@ -316,8 +316,8 @@ export class ConnectionOptionsTabService extends Bootstrap { if ((state.config.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) { tempConfig.authModelId = state.config.authModelId || driver.defaultAuthModel; - tempConfig.saveCredentials = state.config.saveCredentials; tempConfig.sharedCredentials = state.config.sharedCredentials; + tempConfig.saveCredentials = state.config.saveCredentials || tempConfig.sharedCredentials; const properties = await this.getConnectionAuthModelProperties(tempConfig.authModelId, state.info); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index ec25a5f123..12fa37d88d 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useCallback, useRef } from 'react'; +import { useContext, useRef } from 'react'; import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; import { @@ -20,29 +20,29 @@ import { Group, GroupTitle, InputField, - ObjectPropertyInfoForm, + Link, Radio, RadioGroup, s, Textarea, useAdministrationSettings, useFormValidator, - usePermission, useResource, useS, useTranslate, } from '@cloudbeaver/core-blocks'; import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; -import { CachedResourceListEmptyKey, resourceKeyList } from '@cloudbeaver/core-resource'; -import { EAdminPermission, ServerConfigResource } from '@cloudbeaver/core-root'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import { DriverConfigurationType } from '@cloudbeaver/core-sdk'; -import { type TabContainerPanelComponent, useAuthenticationAction } from '@cloudbeaver/core-ui'; -import { isSafari } from '@cloudbeaver/core-utils'; +import { type TabContainerPanelComponent, TabsContext, useAuthenticationAction } from '@cloudbeaver/core-ui'; import { ProjectSelect } from '@cloudbeaver/plugin-projects'; +import { ConnectionAuthModelCredentialsForm } from '../ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm'; +import { ConnectionAuthModelSelector } from '../ConnectionAuthModelCredentials/ConnectionAuthModelSelector'; import { ConnectionFormService } from '../ConnectionFormService'; import type { IConnectionFormProps } from '../IConnectionFormProps'; +import { CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID } from '../SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID'; import { ConnectionOptionsTabService } from './ConnectionOptionsTabService'; import styles from './Options.m.css'; import { ParametersForm } from './ParametersForm'; @@ -77,26 +77,15 @@ export const Options: TabContainerPanelComponent = observe const translate = useTranslate(); const { info, config, availableDrivers, submittingTask: submittingHandlers, disabled } = state; const style = useS(styles); + const tabsState = useContext(TabsContext); //@TODO it's here until the profile implementation in the CloudBeaver const readonly = state.readonly || info?.authModel === PROFILE_AUTH_MODEL_ID; - const adminPermission = usePermission(EAdminPermission.admin); - useFormValidator(submittingHandlers.for(service.formValidationTask), formRef.current); const optionsHook = useOptions(state); const { credentialsSavingEnabled } = useAdministrationSettings(); - const handleAuthModelSelect = useCallback(async (value?: string, name?: string, prev?: string) => { - const model = applicableAuthModels.find(model => model?.id === value); - - if (!model) { - return; - } - - optionsHook.setAuthModel(model); - }, []); - const driverMap = useResource( Options, DBDriverResource, @@ -119,25 +108,11 @@ export const Options: TabContainerPanelComponent = observe if (config.template) { config.folder = undefined; } - - if (name === 'sharedCredentials' && value) { - config.saveCredentials = true; - - for (const handler of config.networkHandlersConfig ?? []) { - if (!handler.savePassword) { - handler.savePassword = true; - } - } - } } - const { data: applicableAuthModels } = useResource( - Options, - DatabaseAuthModelsResource, - driver?.applicableAuthModels ? resourceKeyList(driver.applicableAuthModels) : CachedResourceListEmptyKey, - ); + const applicableAuthModels = driver?.applicableAuthModels ?? []; - const { data: authModel } = useResource( + const authModelLoader = useResource( Options, DatabaseAuthModelsResource, getComputed(() => config.authModelId || info?.authModel || driver?.defaultAuthModel || null), @@ -146,6 +121,22 @@ export const Options: TabContainerPanelComponent = observe }, ); + const authModel = authModelLoader.data; + + async function handleAuthModelSelect(id: string | undefined) { + if (!id) { + return; + } + + const model = await authModelLoader.resource.load(id); + + if (!model) { + return; + } + + optionsHook.setAuthModel(model); + } + const authentication = useAuthenticationAction({ providerId: authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, }); @@ -154,7 +145,6 @@ export const Options: TabContainerPanelComponent = observe const edit = state.mode === 'edit'; const originLocal = !info || isLocalConnection(info); - const availableAuthModels = applicableAuthModels.filter(model => !!model && (adminPermission || !model.requiresLocalConfiguration)); const drivers = driverMap.resource.enabledDrivers.filter(({ id }) => availableDrivers.includes(id)); let properties = authModel?.properties; @@ -163,6 +153,13 @@ export const Options: TabContainerPanelComponent = observe properties = info.authProperties; } + const sharedCredentials = config.sharedCredentials && serverConfigResource.data?.distributed; + + function openCredentialsTab(event: React.MouseEvent) { + event.preventDefault(); + tabsState?.open(CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID); + } + return (
@@ -285,63 +282,56 @@ export const Options: TabContainerPanelComponent = observe {!driver?.anonymousAccess && (authentication.authorized || !edit) && ( {translate('connections_connection_edit_authentication')} - {availableAuthModels.length > 1 && ( - model!.id} - valueSelector={model => model!.displayName} - titleSelector={model => model?.description} - searchable={availableAuthModels.length > 10} - readOnly={readonly || !originLocal} - disabled={disabled} - tiny - fill - onSelect={handleAuthModelSelect} - /> + disabled={disabled || readonly} + keepSize + > + {translate('connections_connection_share_credentials')} + )} - {authModel && properties && ( + + {!sharedCredentials ? ( <> - - - - {credentialsSavingEnabled && !config.template && ( - - - {translate('connections_connection_edit_save_credentials')} - - {serverConfigResource.resource.distributed && connectionOptionsTabService.isProjectShared(state) && ( - - {translate('connections_connection_share_credentials')} - - )} - )} + ) : ( + + {translate('plugin_connections_connection_form_shared_credentials_manage_info')} + + {translate('plugin_connections_connection_form_shared_credentials_manage_info_tab_link')} + + + )} + {!sharedCredentials && authModel && credentialsSavingEnabled && !config.template && ( + + {translate('connections_connection_edit_save_credentials')} + )} )} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts index 927eb5ad68..6a0c8dbef4 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts @@ -176,6 +176,7 @@ export class ConnectionSSHTabService extends Bootstrap { 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, }; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx index 4e8ffb20af..8c594677d3 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx @@ -147,13 +147,8 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st {keyAuth && } - {credentialsSavingEnabled && !formState.config.template && ( - + {credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && ( + {translate('connections_connection_edit_save_credentials')} )} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts index dd3df9c23e..34c58c1551 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts @@ -136,6 +136,7 @@ export class ConnectionSSLTabService extends Bootstrap { 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); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx index cc86b30ebc..68f75ea1fc 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx @@ -102,7 +102,7 @@ export const SSL: TabContainerPanelComponent = observer(function SSL({ st ))} - {credentialsSavingEnabled && !formState.config.template && ( + {credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && ( = observer(function DatabaseAuthDialog({ payload, options, rejectDialog, resolveDialog }) { - const styles = useS(style); + const connectionInfoLoader = useResource(DatabaseAuthDialog, ConnectionInfoResource, { + key: payload.connection, + includes: ['includeAuthNeeded', 'includeSharedSecrets', 'includeNetworkHandlersConfig', 'includeCredentialsSaved'], + }); const translate = useTranslate(); - const [focusedRef] = useFocus({ focusFirstChild: true }); + const driverLoader = useResource(DatabaseAuthDialog, DBDriverResource, connectionInfoLoader.data?.driverId || null); + const useSharedCredentials = (connectionInfoLoader.data?.sharedSecrets?.length || 0) > 1; - const { credentialsSavingEnabled } = useAdministrationSettings(); - const dialog = useDatabaseAuthDialog(payload.connection, payload.networkHandlers, payload.resetCredentials, resolveDialog); - const errorDetails = useErrorDetails(dialog.authException); + let subtitle = connectionInfoLoader.data?.name; - useAutoLoad(DatabaseAuthDialog, dialog); + if (useSharedCredentials) { + subtitle = [subtitle, translate('plugin_connections_connection_auth_secret_description')].join(' | '); + } return ( - + - - - - - - - - - - {dialog.authException && ( - - )} - - + {useSharedCredentials ? ( + + ) : ( + + )} ); }); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.m.css b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.m.css similarity index 100% rename from webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.m.css rename to webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.m.css diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.tsx new file mode 100644 index 0000000000..2aebbf2f87 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.tsx @@ -0,0 +1,87 @@ +/* + * 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 { observer } from 'mobx-react-lite'; + +import { + CommonDialogBody, + CommonDialogFooter, + ErrorMessage, + Form, + Loader, + s, + useAdministrationSettings, + useAutoLoad, + useErrorDetails, + useFocus, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import type { IConnectionInfoParams } from '@cloudbeaver/core-connections'; + +import { ConnectionAuthenticationFormLoader } from '../../ConnectionAuthentication/ConnectionAuthenticationFormLoader'; +import style from './DatabaseCredentialsAuthDialog.m.css'; +import { DatabaseCredentialsAuthDialogFooter } from './DatabaseCredentialsAuthDialogFooter'; +import { useDatabaseCredentialsAuthDialog } from './useDatabaseCredentialsAuthDialog'; + +interface Props { + connection: IConnectionInfoParams; + networkHandlers: string[]; + resetCredentials?: boolean; + onLogin?: () => void; +} + +export const DatabaseCredentialsAuthDialog = observer(function DatabaseCredentialsAuthDialog({ + connection, + networkHandlers, + resetCredentials, + onLogin, +}) { + const styles = useS(style); + const translate = useTranslate(); + const [focusedRef] = useFocus({ focusFirstChild: true }); + + const { credentialsSavingEnabled } = useAdministrationSettings(); + const dialog = useDatabaseCredentialsAuthDialog(connection, networkHandlers, resetCredentials, onLogin); + const errorDetails = useErrorDetails(dialog.authException); + + useAutoLoad(DatabaseCredentialsAuthDialog, dialog); + + return ( + <> + +
+ + + +
+
+ + + {dialog.authException && ( + + )} + + + + ); +}); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.tsx similarity index 89% rename from webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx rename to webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.tsx index 67dc57ac5d..79b537bf54 100644 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.tsx @@ -32,7 +32,7 @@ export interface Props { className?: string; } -export const DBAuthDialogFooter = observer>(function DBAuthDialogFooter({ +export const DatabaseCredentialsAuthDialogFooter = observer>(function DatabaseCredentialsAuthDialogFooter({ isAuthenticating, onLogin, className, diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/useDatabaseAuthDialog.ts b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/useDatabaseCredentialsAuthDialog.ts similarity index 94% rename from webapp/packages/plugin-connections/src/DatabaseAuthDialog/useDatabaseAuthDialog.ts rename to webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/useDatabaseCredentialsAuthDialog.ts index fdb55bcb03..06c9378c10 100644 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/useDatabaseAuthDialog.ts +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/useDatabaseCredentialsAuthDialog.ts @@ -21,7 +21,7 @@ import { useService } from '@cloudbeaver/core-di'; import { NetworkHandlerAuthType } from '@cloudbeaver/core-sdk'; import type { ILoadableState } from '@cloudbeaver/core-utils'; -import type { IConnectionAuthenticationConfig } from '../ConnectionAuthentication/IConnectionAuthenticationConfig'; +import type { IConnectionAuthenticationConfig } from '../../ConnectionAuthentication/IConnectionAuthenticationConfig'; interface IState extends ILoadableState { readonly authModelId: string | null; @@ -38,7 +38,12 @@ interface IState extends ILoadableState { getConfig: () => ConnectionInitConfig; } -export function useDatabaseAuthDialog(key: IConnectionInfoParams, networkHandlers: string[], resetCredentials?: boolean, onInit?: () => void) { +export function useDatabaseCredentialsAuthDialog( + key: IConnectionInfoParams, + networkHandlers: string[], + resetCredentials?: boolean, + onInit?: () => void, +) { const connectionInfoResource = useService(ConnectionInfoResource); const dbDriverResource = useService(DBDriverResource); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.tsx new file mode 100644 index 0000000000..c5c09f4125 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.tsx @@ -0,0 +1,88 @@ +/* + * 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 } from 'mobx'; +import { observer } from 'mobx-react-lite'; + +import { + CommonDialogBody, + CommonDialogFooter, + ExceptionMessage, + Group, + ItemList, + ListItem, + ListItemName, + Loader, + useObservableRef, + useResource, +} from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; + +interface Props { + connectionKey: IConnectionInfoParams; + onLogin?: () => void; +} + +export const DatabaseSecretAuthDialog = observer(function DatabaseSecretAuthDialog({ connectionKey, onLogin }) { + const connectionInfoLoader = useResource(DatabaseSecretAuthDialog, ConnectionInfoResource, { + key: connectionKey, + includes: ['includeAuthNeeded', 'includeSharedSecrets', 'includeNetworkHandlersConfig', 'includeCredentialsSaved'], + }); + const state = useObservableRef( + () => ({ + exception: null as Error | null, + authenticating: false, + }), + { + exception: observable.ref, + authenticating: observable.ref, + }, + false, + ); + const secrets = connectionInfoLoader.data?.sharedSecrets || []; + + async function handleSecretSelect(secretId: string) { + try { + state.authenticating = true; + await connectionInfoLoader.resource.init({ + ...connectionKey, + selectedSecretId: secretId, + }); + state.exception = null; + onLogin?.(); + } catch (exception: any) { + state.exception = exception; + } finally { + state.authenticating = false; + } + } + + if (state.authenticating) { + return ; + } + + return ( + <> + + + {secrets.map(secret => ( + handleSecretSelect(secret.secretId)}> + {secret.displayName} + + ))} + + + {state.exception && ( + + + + + + )} + + ); +}); diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts index a0b4ace7f8..272bf0747f 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts @@ -8,8 +8,8 @@ import { action, makeObservable, observable } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, ConnectionsManagerService, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -20,11 +20,11 @@ import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; import { OptionsPanelService } from '@cloudbeaver/core-ui'; import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; -import { ConnectionAuthService } from '../ConnectionAuthService'; import { ConnectionFormService } from '../ConnectionForm/ConnectionFormService'; import { ConnectionFormState } from '../ConnectionForm/ConnectionFormState'; import type { IConnectionFormState } from '../ConnectionForm/IConnectionFormProps'; -import { PublicConnectionForm } from './PublicConnectionForm'; + +const PublicConnectionForm = importLazyComponent(() => import('./PublicConnectionForm').then(m => m.PublicConnectionForm)); const formGetter = () => PublicConnectionForm; @@ -38,7 +38,7 @@ export class PublicConnectionFormService { private readonly optionsPanelService: OptionsPanelService, private readonly connectionFormService: ConnectionFormService, private readonly connectionInfoResource: ConnectionInfoResource, - private readonly connectionAuthService: ConnectionAuthService, + private readonly connectionsManagerService: ConnectionsManagerService, private readonly userInfoResource: UserInfoResource, private readonly authenticationService: AuthenticationService, private readonly projectsService: ProjectsService, @@ -203,7 +203,7 @@ export class PublicConnectionFormService { try { await this.connectionInfoResource.close(connectionKey); - await this.connectionAuthService.auth(connectionKey); + await this.connectionsManagerService.requireConnection(connectionKey); } catch (exception: any) { this.notificationService.logException(exception, 'connections_public_connection_edit_reconnect_failed'); } diff --git a/webapp/packages/plugin-connections/src/index.ts b/webapp/packages/plugin-connections/src/index.ts index f2e9d6b180..9d5de586c7 100644 --- a/webapp/packages/plugin-connections/src/index.ts +++ b/webapp/packages/plugin-connections/src/index.ts @@ -7,6 +7,7 @@ export * from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabSe export * from './ConnectionForm/SSH/ConnectionSSHTabService'; export * from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService'; export * from './ConnectionForm/Contexts/connectionConfigContext'; +export * from './ConnectionForm/Contexts/connectionCredentialsStateContext'; export * from './ConnectionForm/ConnectionFormBaseActionsLoader'; export * from './ConnectionForm/connectionFormConfigureContext'; export * from './ConnectionForm/ConnectionFormLoader'; @@ -14,6 +15,8 @@ export * from './ConnectionForm/ConnectionFormService'; export * from './ConnectionForm/ConnectionFormState'; export * from './ConnectionForm/IConnectionFormProps'; export * from './ConnectionForm/useConnectionFormState'; +export * from './ConnectionForm/SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID'; +export * from './ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm'; export * from './ContextMenu/MENU_CONNECTION_VIEW'; export * from './ContextMenu/MENU_CONNECTIONS'; export * from './PublicConnectionForm/PublicConnectionFormService'; diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index 98b3b99594..6c87ad975f 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -20,4 +20,8 @@ export default [ ['settings_connections_hide_connections_view_description', 'Show connections to admins only'], ['connections_public_connection_ssl_enable', 'Enable SSL'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', 'You can manage credentials in the '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Credentials tab'], + ['plugin_connections_connection_auth_secret_description', 'Please select credentials provided by one of your teams'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/it.ts b/webapp/packages/plugin-connections/src/locales/it.ts index c13c7bda6c..f95e5aceee 100644 --- a/webapp/packages/plugin-connections/src/locales/it.ts +++ b/webapp/packages/plugin-connections/src/locales/it.ts @@ -22,4 +22,8 @@ export default [ ['settings_connections_hide_connections_view_description', 'Show connections to admins only'], ['connections_public_connection_ssl_enable', 'Enable SSL'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', 'You can manage credentials in the '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Credentials tab'], + ['plugin_connections_connection_auth_secret_description', 'Please select credentials provided by one of your teams'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index 711186564a..9ba585075f 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -19,4 +19,8 @@ export default [ ['settings_connections_hide_connections_view_description', 'Показывать подключения только администраторам'], ['connections_public_connection_ssl_enable', 'Включить SSL'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', 'Вы можете указать учетные данные в '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'во вкладке "Учетные данные"'], + ['plugin_connections_connection_auth_secret_description', 'Выберете учетные данные, предоставленные одной из ваших команд'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/zh.ts b/webapp/packages/plugin-connections/src/locales/zh.ts index e8c876924c..ef31d2598f 100644 --- a/webapp/packages/plugin-connections/src/locales/zh.ts +++ b/webapp/packages/plugin-connections/src/locales/zh.ts @@ -19,4 +19,8 @@ export default [ ['settings_connections_hide_connections_view_description', 'Show connections to admins only'], ['connections_public_connection_ssl_enable', 'Enable SSL'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', 'You can manage credentials in the '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Credentials tab'], + ['plugin_connections_connection_auth_secret_description', 'Please select credentials provided by one of your teams'], ]; diff --git a/webapp/packages/plugin-connections/src/manifest.ts b/webapp/packages/plugin-connections/src/manifest.ts index 4afa470555..ccdfe2fe77 100644 --- a/webapp/packages/plugin-connections/src/manifest.ts +++ b/webapp/packages/plugin-connections/src/manifest.ts @@ -13,13 +13,13 @@ import { ConnectionDriverPropertiesTabService } from './ConnectionForm/DriverPro import { ConnectionOptionsTabService } from './ConnectionForm/Options/ConnectionOptionsTabService'; import { ConnectionOriginInfoTabService } from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService'; import { ConnectionSSHTabService } from './ConnectionForm/SSH/ConnectionSSHTabService'; +import { ConnectionSSLTabService } from './ConnectionForm/SSL/ConnectionSSLTabService'; import { ConnectionMenuBootstrap } from './ContextMenu/ConnectionMenuBootstrap'; import { LocaleService } from './LocaleService'; import { ConnectionFoldersBootstrap } from './NavNodes/ConnectionFoldersBootstrap'; import { PluginBootstrap } from './PluginBootstrap'; import { PluginConnectionsSettingsService } from './PluginConnectionsSettingsService'; import { PublicConnectionFormService } from './PublicConnectionForm/PublicConnectionFormService'; -import { ConnectionSSLTabService } from './ConnectionForm/SSL/ConnectionSSLTabService'; export const connectionPlugin: PluginManifest = { info: { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx index a8fa44907b..30bae2860f 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx @@ -27,7 +27,7 @@ import style from './FilterCustomValueDialog.m.css'; interface IPayload { inputTitle: string; - defaultValue: string | number; + defaultValue: string; } export const FilterCustomValueDialog: DialogComponent = observer(function FilterCustomValueDialog({ @@ -39,7 +39,7 @@ export const FilterCustomValueDialog: DialogComponent const styles = useS(style); const inputRef = useRef(null); - const [value, setValue] = useState(payload.defaultValue); + const [value, setValue] = useState(payload.defaultValue); const handleApply = useCallback(() => resolveDialog(value), [value, resolveDialog]); const translate = useTranslate(); diff --git a/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx b/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx index cb7f4ecb62..dd4d045432 100644 --- a/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx +++ b/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx @@ -6,11 +6,11 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import styled, { css } from 'reshadow'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { TextPlaceholder, useExecutor, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { Loader, TextPlaceholder, useExecutor, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { BASE_TAB_STYLES, ITabData, TabPanel, TabsBox } from '@cloudbeaver/core-ui'; import { CaptureView } from '@cloudbeaver/core-view'; @@ -47,6 +47,7 @@ export const NavigationTabsBar = observer(function NavigationTabsBar({ cl // TODO: we get exception when after closing the restored page trying to open another // it's related to hooks order and state restoration const style = useStyles(BASE_TAB_STYLES, styles); + const [restoring, setRestoring] = useState(false); const translate = useTranslate(); const handleSelect = useCallback((tabId: string) => navigation.selectTab(tabId), [navigation]); @@ -57,7 +58,12 @@ export const NavigationTabsBar = observer(function NavigationTabsBar({ cl } async function restoreTabs() { - await navigation.restoreTabs(); + setRestoring(true); + try { + await navigation.restoreTabs(); + } finally { + setRestoring(false); + } } function handleTabChange(tab: ITabData) { @@ -85,24 +91,26 @@ export const NavigationTabsBar = observer(function NavigationTabsBar({ cl return styled(style)( - ( - - ))} - tabList={navigation.tabIdList} - style={styles} - tabIndex={0} - autoSelect - enabledBaseActions - onChange={handleTabChange} - > - {navigation.tabIdList.map(tabId => ( - - {() => } - - ))} - + + ( + + ))} + tabList={navigation.tabIdList} + style={styles} + tabIndex={0} + autoSelect + enabledBaseActions + onChange={handleTabChange} + > + {navigation.tabIdList.map(tabId => ( + + {() => } + + ))} + + , ); }); diff --git a/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts b/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts index 38c60dd002..bad5d7c8e4 100644 --- a/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts @@ -5,6 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { getCachedDataResourceLoaderState } from '@cloudbeaver/core-resource'; import { ServerConfigResource } from '@cloudbeaver/core-root'; @@ -13,9 +14,10 @@ import { ActionService, DATA_CONTEXT_MENU, menuExtractItems, MenuService } from import { MENU_TOOLS } from '@cloudbeaver/plugin-tools-panel'; import { ACTION_RESOURCE_MANAGER_SCRIPTS } from './Actions/ACTION_RESOURCE_MANAGER_SCRIPTS'; -import { ResourceManagerScripts } from './ResourceManagerScripts'; import { ResourceManagerScriptsService } from './ResourceManagerScriptsService'; +const ResourceManagerScripts = importLazyComponent(() => import('./ResourceManagerScripts').then(m => m.ResourceManagerScripts)); + @injectable() export class PluginBootstrap extends Bootstrap { constructor( @@ -57,9 +59,12 @@ export class PluginBootstrap extends Bootstrap { isActionApplicable: (context, action) => [ACTION_RESOURCE_MANAGER_SCRIPTS].includes(action), isHidden: () => !this.resourceManagerScriptsService.enabled, isChecked: () => this.resourceManagerScriptsService.active, - getLoader: (context, action) => { - return getCachedDataResourceLoaderState(this.serverConfigResource, undefined, undefined); - }, + getLoader: () => + getCachedDataResourceLoaderState( + this.serverConfigResource, + () => undefined, + () => undefined, + ), handler: (context, action) => { switch (action) { case ACTION_RESOURCE_MANAGER_SCRIPTS: { diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx index a65d27163e..590fd2cbd3 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx @@ -23,7 +23,15 @@ export const ChangePassword = observer(function ChangePassword() {
{translate('plugin_user_profile_authentication_change_password')} - value.trim()} small required> + value?.trim() ?? ''} + small + required + > {translate('plugin_user_profile_authentication_change_password_current_password')} value.trim()} + mapValue={(value?: string) => value?.trim() ?? ''} small required > {translate('plugin_user_profile_authentication_change_password_new_password')} - value.trim()} small required> + value?.trim() ?? ''} + small + required + > {translate('plugin_user_profile_authentication_change_password_repeat_password')} -