mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
CB-4561 te UI dynamic connection credentials based on the users team (#2381)
* CB-4598 npe fix * CB-4561 refactor: connection info resource * CB-4561 refactor: connection info resource * CB-4561 load credentials from secret in initConnection * CB-4561 merge devel * CB-4561 allow initial team secrets test * CB-4561 remove ability to use any secret in test connection * CB-4561 feat: shared credentials management * CB-4561 feat: connection secret authentication * CB-4561 little fix * CB-4561 feat: shared credentials * CB-4561 chore: remove save credentials checkbox for shared credentials * CB-4561 fix: send saved state for shared connection credentials * CB-4561 feat: change shared secret authentication description * CB-4561 fix: pr review comments * CB-4561 fix: disable connection test for shared connections in distributed env * CB-4561 fix: types --------- Co-authored-by: Aleksandr Skoblikov <aleksandr.skoblikov@dbeaver.com> Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
co-authored by
Aleksandr Skoblikov
mr-anton-t
parent
662d991aca
commit
2b90eb038e
@@ -448,5 +448,12 @@ public class WebConnectionInfo {
|
||||
return dataSourceContainer.getConnectionConfiguration().getKeepAliveInterval();
|
||||
}
|
||||
|
||||
@Property
|
||||
public List<WebSecretInfo> getSharedSecrets() throws DBException {
|
||||
return dataSourceContainer.listSharedCredentials()
|
||||
.stream()
|
||||
.map(WebSecretInfo::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1008,6 +1008,17 @@ public class WebSession extends BaseWebSession
|
||||
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<WebProjectImpl> getAccessibleProjects() {
|
||||
return getWorkspace().getProjects();
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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<String, Object> providerProperties;
|
||||
private List<WebNetworkHandlerConfigInput> networkHandlersConfig;
|
||||
private DBPDriverConfigurationType configurationType;
|
||||
private String selectedSecretId;
|
||||
|
||||
public WebConnectionConfig() {
|
||||
}
|
||||
@@ -91,6 +93,7 @@ public class WebConnectionConfig {
|
||||
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 class WebConnectionConfig {
|
||||
public Integer getKeepAliveInterval() {
|
||||
return keepAliveInterval;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSelectedSecretId() {
|
||||
return selectedSecretId;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -113,7 +113,8 @@ public interface DBWServiceCore extends DBWService {
|
||||
@NotNull Map<String, Object> authProperties,
|
||||
@Nullable List<WebNetworkHandlerConfigInput> networkCredentials,
|
||||
@Nullable Boolean saveCredentials,
|
||||
@Nullable Boolean sharedCredentials
|
||||
@Nullable Boolean sharedCredentials,
|
||||
@Nullable String selectedCredentials
|
||||
) throws DBWebException;
|
||||
|
||||
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
|
||||
|
||||
+2
-1
@@ -132,7 +132,8 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
|
||||
env.getArgument("credentials"),
|
||||
nhc,
|
||||
env.getArgument("saveCredentials"),
|
||||
env.getArgument("sharedCredentials")
|
||||
env.getArgument("sharedCredentials"),
|
||||
env.getArgument("selectedSecretId")
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
+40
-6
@@ -54,6 +54,7 @@ import org.jkiss.dbeaver.model.net.ssh.SSHImplementation;
|
||||
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 class WebServiceCore implements DBWServiceCore {
|
||||
@NotNull Map<String, Object> authProperties,
|
||||
@Nullable List<WebNetworkHandlerConfigInput> 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<DBSSecretValue> 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 class WebServiceCore implements DBWServiceCore {
|
||||
|
||||
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 class WebServiceCore implements DBWServiceCore {
|
||||
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 class WebServiceCore implements DBWServiceCore {
|
||||
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
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32ZM10.9493 14.6232L9.5 16.5072L14.8623 21L22.5435 12.5942L20.8043 11L14.7174 17.8116L10.9493 14.6232Z" fill="#52C41A"/>
|
||||
<path class="icon-fill" id="root" fill-rule="evenodd" clip-rule="evenodd" d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32ZM10.9493 14.6232L9.5 16.5072L14.8623 21L22.5435 12.5942L20.8043 11L14.7174 17.8116L10.9493 14.6232Z" fill="#52C41A"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 383 B After Width: | Height: | Size: 412 B |
@@ -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';
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
.group {
|
||||
composes: theme-background-surface theme-text-on-surface from global;
|
||||
/*
|
||||
* 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;
|
||||
|
||||
@@ -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<HTMLDivElement, Props & React.HTMLAttributes<HTMLDivElement>>(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<HTMLDivElement, Props & React.HTMLAttributes<HTM
|
||||
...containerProps,
|
||||
group: true,
|
||||
container: true,
|
||||
secondary,
|
||||
surface: !secondary,
|
||||
form,
|
||||
center,
|
||||
boxNoOverflow,
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
.field.fill,
|
||||
.container.fill,
|
||||
.container > .fill {
|
||||
flex-grow: 1;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Props>(function ExceptionMessage({ exce
|
||||
};
|
||||
}
|
||||
|
||||
if (!exception) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s(styles, { error: true, icon, inline }, className)}>
|
||||
<div className={s(styles, { errorIcon: true })} title={error.message}>
|
||||
@@ -51,7 +55,8 @@ export const ExceptionMessage = observer<Props>(function ExceptionMessage({ exce
|
||||
<span>{translate('core_blocks_exception_message_error_title')}</span>
|
||||
</h2>
|
||||
<div className={s(styles, { errorMessage: true })}>
|
||||
{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')}
|
||||
</div>
|
||||
<div className={s(styles, { errorActions: true })}>
|
||||
<Button type="button" mod={['outlined']} disabled={error.isOpen} onClick={error.open}>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
& .fieldLabel {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding-left: 10px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ILayoutSizeProps } from '../../Containers/ILayoutSizeProps';
|
||||
import { s } from '../../s';
|
||||
import { useS } from '../../useS';
|
||||
import { Field } from '../Field';
|
||||
import { FieldDescription } from '../FieldDescription';
|
||||
import { FieldLabel } from '../FieldLabel';
|
||||
import { isControlPresented } from '../isControlPresented';
|
||||
import { Checkbox, CheckboxBaseProps, CheckboxType, ICheckboxControlledProps, ICheckboxObjectProps } from './Checkbox';
|
||||
|
||||
@@ -13,7 +13,7 @@ import { s } from '../s';
|
||||
import { useFocus } from '../useFocus';
|
||||
import { useS } from '../useS';
|
||||
import filterStyle from './Filter.m.css';
|
||||
import { InputField } from './InputField';
|
||||
import { InputField } from './InputField/InputField';
|
||||
|
||||
interface BaseProps {
|
||||
placeholder?: string;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface IFormStateControl<TState extends Record<string, any>, TKey extends keyof TState> {
|
||||
name: TKey;
|
||||
state: TState;
|
||||
defaultState?: TState;
|
||||
autoHide?: boolean;
|
||||
mapState?: (value: TState[TKey]) => string;
|
||||
mapValue?: (value: string) => TState[TKey];
|
||||
onChange?: (value: TState[TKey], name: TKey) => any;
|
||||
}
|
||||
+4
-1
@@ -32,10 +32,13 @@
|
||||
|
||||
.customIconContainer {
|
||||
composes: iconContainer;
|
||||
right: 4px;
|
||||
right: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.input[disabled] + .iconContainer {
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 React, { forwardRef, useContext } from 'react';
|
||||
|
||||
import { useCombinedHandler } from '../../useCombinedHandler';
|
||||
import { FormContext } from '../FormContext';
|
||||
import { isFormStateControl } from '../isFormStateControl';
|
||||
import { InputFieldBase, type InputFieldBaseProps } from './InputFieldBase';
|
||||
import { InputFieldState, type InputFieldStateProps } from './InputFieldState';
|
||||
|
||||
interface InputFieldType {
|
||||
(props: InputFieldBaseProps): React.ReactElement<any, any> | null;
|
||||
<TState extends Record<string, any>, TKey extends keyof TState>(props: InputFieldStateProps<TState, TKey>): React.ReactElement<any, any> | null;
|
||||
}
|
||||
|
||||
export const InputField: InputFieldType = observer<InputFieldBaseProps | InputFieldStateProps<any, any>, HTMLInputElement>(
|
||||
forwardRef(function InputField({ onChange, onKeyDown, ...rest }, ref) {
|
||||
const context = useContext(FormContext);
|
||||
|
||||
const handleChange = useCombinedHandler(onChange, context?.change);
|
||||
const handleKeyDown = useCombinedHandler(onKeyDown, context?.keyDown);
|
||||
|
||||
if (isFormStateControl(rest)) {
|
||||
return <InputFieldState ref={ref} {...rest} onChange={handleChange} onKeyDown={handleKeyDown} />;
|
||||
}
|
||||
|
||||
return <InputFieldBase ref={ref} {...rest} onChange={handleChange} onKeyDown={handleKeyDown} />;
|
||||
}),
|
||||
) as InputFieldType;
|
||||
+30
-81
@@ -6,82 +6,53 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { forwardRef, useCallback, useContext, useLayoutEffect, useRef, useState } from 'react';
|
||||
import React, { forwardRef, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
import { isNotNullDefined } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
|
||||
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
|
||||
import { Icon } from '../Icon';
|
||||
import { Loader } from '../Loader/Loader';
|
||||
import { useTranslate } from '../localization/useTranslate';
|
||||
import { s } from '../s';
|
||||
import { useCombinedHandler } from '../useCombinedHandler';
|
||||
import { useCombinedRef } from '../useCombinedRef';
|
||||
import { useS } from '../useS';
|
||||
import { useStateDelay } from '../useStateDelay';
|
||||
import { Field } from './Field';
|
||||
import { FieldDescription } from './FieldDescription';
|
||||
import { FieldLabel } from './FieldLabel';
|
||||
import { FormContext } from './FormContext';
|
||||
import { filterLayoutFakeProps, getLayoutProps } from '../../Containers/filterLayoutFakeProps';
|
||||
import type { ILayoutSizeProps } from '../../Containers/ILayoutSizeProps';
|
||||
import { Icon } from '../../Icon';
|
||||
import { Loader } from '../../Loader/Loader';
|
||||
import { useTranslate } from '../../localization/useTranslate';
|
||||
import { s } from '../../s';
|
||||
import { useCombinedHandler } from '../../useCombinedHandler';
|
||||
import { useCombinedRef } from '../../useCombinedRef';
|
||||
import { useS } from '../../useS';
|
||||
import { useStateDelay } from '../../useStateDelay';
|
||||
import { Field } from '../Field';
|
||||
import { FieldDescription } from '../FieldDescription';
|
||||
import { FieldLabel } from '../FieldLabel';
|
||||
import { useCapsLockTracker } from '../useCapsLockTracker';
|
||||
import inputFieldStyle from './InputField.m.css';
|
||||
import { isControlPresented } from './isControlPresented';
|
||||
import { useCapsLockTracker } from './useCapsLockTracker';
|
||||
|
||||
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'name' | 'value' | 'style'> &
|
||||
export type InputFieldBaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'style'> &
|
||||
ILayoutSizeProps & {
|
||||
value?: string;
|
||||
error?: boolean;
|
||||
loading?: boolean;
|
||||
description?: string;
|
||||
labelTooltip?: string;
|
||||
ref?: React.ForwardedRef<HTMLInputElement>;
|
||||
canShowPassword?: boolean;
|
||||
onCustomCopy?: () => void;
|
||||
icon?: React.ReactElement;
|
||||
onCustomCopy?: () => void;
|
||||
onChange?: (value: string, name?: string) => any;
|
||||
};
|
||||
|
||||
type ControlledProps = BaseProps & {
|
||||
name?: string;
|
||||
value?: string | number;
|
||||
mapState?: (value: string | number) => string | number;
|
||||
mapValue?: (value: string | number) => string | number;
|
||||
onChange?: (value: string | number, name?: string) => any;
|
||||
state?: never;
|
||||
autoHide?: never;
|
||||
};
|
||||
|
||||
type ObjectProps<TKey extends keyof TState, TState> = BaseProps & {
|
||||
name: TKey;
|
||||
state: TState;
|
||||
mapState?: (value: TState[TKey]) => TState[TKey] | string | number;
|
||||
mapValue?: (value: TState[TKey]) => TState[TKey];
|
||||
onChange?: (value: TState[TKey], name: TKey) => any;
|
||||
autoHide?: boolean;
|
||||
value?: never;
|
||||
};
|
||||
|
||||
interface InputFieldType {
|
||||
(props: ControlledProps): React.ReactElement<any, any> | null;
|
||||
<TKey extends keyof TState, TState>(props: ObjectProps<TKey, TState>): React.ReactElement<any, any> | null;
|
||||
}
|
||||
|
||||
export const InputField: InputFieldType = observer<ControlledProps | ObjectProps<any, any>, HTMLInputElement>(
|
||||
forwardRef(function InputField(
|
||||
export const InputFieldBase = observer<InputFieldBaseProps, HTMLInputElement>(
|
||||
forwardRef(function InputFieldBase(
|
||||
{
|
||||
name,
|
||||
value: valueControlled,
|
||||
value,
|
||||
defaultValue,
|
||||
required,
|
||||
state,
|
||||
mapState,
|
||||
mapValue,
|
||||
children,
|
||||
className,
|
||||
error,
|
||||
loading,
|
||||
description,
|
||||
labelTooltip,
|
||||
autoHide,
|
||||
canShowPassword = true,
|
||||
onChange,
|
||||
onCustomCopy,
|
||||
@@ -98,7 +69,6 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
|
||||
const layoutProps = getLayoutProps(rest);
|
||||
rest = filterLayoutFakeProps(rest);
|
||||
const styles = useS(inputFieldStyle);
|
||||
const context = useContext(FormContext);
|
||||
loading = useStateDelay(loading ?? false, 300);
|
||||
|
||||
const revealPassword = useCallback(() => {
|
||||
@@ -111,42 +81,25 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = mapValue?.(event.target.value) ?? event.target.value;
|
||||
|
||||
if (state) {
|
||||
state[name] = value;
|
||||
}
|
||||
if (onChange) {
|
||||
onChange(value, name);
|
||||
}
|
||||
if (context) {
|
||||
context.change(value, name);
|
||||
}
|
||||
onChange?.(event.target.value, name);
|
||||
},
|
||||
[state, name, context, onChange],
|
||||
[name, onChange],
|
||||
);
|
||||
|
||||
const handleBlur = useCombinedHandler(rest.onBlur, capsLock.handleBlur);
|
||||
const handleKeyDown = useCombinedHandler(rest.onKeyDown, capsLock.handleKeyDown, context?.keyDown);
|
||||
const handleKeyDown = useCombinedHandler(rest.onKeyDown, capsLock.handleKeyDown);
|
||||
|
||||
const passwordType = rest.type === 'password';
|
||||
let uncontrolled = passwordType && !canShowPassword;
|
||||
|
||||
let value: any = valueControlled ?? undefined;
|
||||
|
||||
if (state && name !== undefined && name in state) {
|
||||
value = state[name];
|
||||
}
|
||||
|
||||
if (mapState) {
|
||||
value = mapState(value);
|
||||
}
|
||||
|
||||
if (passwordType && !rest.readOnly && capsLock.warn) {
|
||||
description = translate('ui_capslock_on');
|
||||
}
|
||||
|
||||
uncontrolled ||= value === undefined;
|
||||
if (!value) {
|
||||
canShowPassword = false;
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (uncontrolled && isNotNullDefined(value) && inputRef.current) {
|
||||
@@ -154,10 +107,6 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
|
||||
}
|
||||
});
|
||||
|
||||
if (autoHide && !isControlPresented(name, state, defaultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Field {...layoutProps} className={s(styles, {}, className)}>
|
||||
<FieldLabel title={labelTooltip || rest.title} className={s(styles, { fieldLabel: true })} required={required}>
|
||||
@@ -169,7 +118,7 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
|
||||
{...rest}
|
||||
type={passwordRevealed ? 'text' : rest.type}
|
||||
name={name}
|
||||
value={uncontrolled ? undefined : value ?? ''}
|
||||
value={uncontrolled ? undefined : value}
|
||||
defaultValue={defaultValue}
|
||||
className={s(styles, { input: true })}
|
||||
required={required}
|
||||
@@ -198,4 +147,4 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
|
||||
</Field>
|
||||
);
|
||||
}),
|
||||
) as InputFieldType;
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 React, { forwardRef } from 'react';
|
||||
|
||||
import type { IFormStateControl } from '../IFormStateControl';
|
||||
import { useFormStateControl } from '../useFormStateControl';
|
||||
import { InputFieldBase, InputFieldBaseProps } from './InputFieldBase';
|
||||
|
||||
export type InputFieldStateProps<TState extends Record<string, any>, TKey extends keyof TState> = Omit<InputFieldBaseProps, 'value'> &
|
||||
IFormStateControl<TState, TKey>;
|
||||
|
||||
interface InputFieldType {
|
||||
<TState extends Record<string, any>, TKey extends keyof TState>(props: InputFieldStateProps<TState, TKey>): React.ReactElement<any, any> | null;
|
||||
}
|
||||
|
||||
export const InputFieldState: InputFieldType = observer<InputFieldStateProps<any, any>, HTMLInputElement>(
|
||||
forwardRef(function InputFieldState({ name, state, defaultState, mapState, mapValue, autoHide, onChange, ...rest }, ref) {
|
||||
const controlState = useFormStateControl({ name, defaultState, state, autoHide, mapState, mapValue, onChange });
|
||||
|
||||
if (controlState.hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InputFieldBase
|
||||
ref={ref}
|
||||
name={name}
|
||||
value={controlState.stringValue ?? controlState.defaultValue ?? ''}
|
||||
onChange={controlState.onChange}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
) as InputFieldType;
|
||||
@@ -8,8 +8,6 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { forwardRef, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import type { ComponentStyle } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { Button } from '../Button';
|
||||
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
|
||||
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { isNotNullDefined } from '@cloudbeaver/core-utils';
|
||||
|
||||
export function isControlPresented(name: string | undefined, state: any, defaultValue?: string | number | readonly string[]): boolean {
|
||||
if (state !== undefined && name !== undefined) {
|
||||
export function isControlPresented(name: string | undefined, state: any, defaultValue?: any): boolean {
|
||||
if (isNotNullDefined(state) && isNotNullDefined(name)) {
|
||||
if (name in state) {
|
||||
return state[name] !== null && state[name] !== undefined;
|
||||
return isNotNullDefined(state[name]);
|
||||
}
|
||||
return defaultValue !== undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 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 type { IFormStateControl } from './IFormStateControl';
|
||||
|
||||
export function isFormStateControl<TState extends Record<string, any>, TKey extends keyof TState>(
|
||||
props: any,
|
||||
): props is IFormStateControl<TState, TKey> {
|
||||
return 'name' in props && 'state' in props;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 { isNotNullDefined, isObject } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { useCombinedHandler } from '../useCombinedHandler';
|
||||
import type { IFormStateControl } from './IFormStateControl';
|
||||
import { isControlPresented } from './isControlPresented';
|
||||
|
||||
export function useFormStateControl<TState extends Record<string, any>, TKey extends keyof TState>({
|
||||
name,
|
||||
state: value,
|
||||
defaultState: defaultValue,
|
||||
mapState: mapToString,
|
||||
mapValue: mapToValue,
|
||||
onChange,
|
||||
...rest
|
||||
}: IFormStateControl<TState, TKey>) {
|
||||
const originalValue = value;
|
||||
const handleChange = useCombinedHandler(function handleChange(inputValue: string) {
|
||||
if (mapToValue) {
|
||||
inputValue = mapToValue(inputValue);
|
||||
}
|
||||
|
||||
if (isNotNullDefined(originalValue) && isObject(originalValue)) {
|
||||
(originalValue as any)[name] = inputValue;
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(inputValue as any, name as any);
|
||||
}
|
||||
});
|
||||
if (isNotNullDefined(defaultValue) && isObject(defaultValue)) {
|
||||
defaultValue = defaultValue[name];
|
||||
}
|
||||
|
||||
if (isNotNullDefined(value) && isObject(value)) {
|
||||
value = value[name];
|
||||
}
|
||||
|
||||
let stringValue: string | typeof value;
|
||||
let defaultStringValue: string | typeof defaultValue;
|
||||
if (mapToString) {
|
||||
stringValue = mapToString(value as any);
|
||||
defaultStringValue = mapToString(defaultValue as any);
|
||||
} else {
|
||||
stringValue = value;
|
||||
defaultStringValue = defaultValue;
|
||||
}
|
||||
|
||||
const hide = 'autoHide' in rest && !!rest.autoHide && !isControlPresented(String(name), stringValue, defaultStringValue);
|
||||
|
||||
return { name, value, stringValue, defaultValue, defaultStringValue, hide, onChange: handleChange };
|
||||
}
|
||||
@@ -18,14 +18,15 @@ export interface IconOrImageProps {
|
||||
title?: string;
|
||||
onClick?: () => void;
|
||||
viewBox?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const IconOrImage: React.FC<IconOrImageProps> = function IconOrImage({ icon, className, title, onClick, viewBox }) {
|
||||
export const IconOrImage: React.FC<IconOrImageProps> = function IconOrImage({ icon, className, title, onClick, viewBox, width }) {
|
||||
const isStaticIcon = useMemo(() => icon && (icon.startsWith('platform:') || icon.startsWith('/')), [icon]);
|
||||
|
||||
if (isStaticIcon || isValidUrl(icon)) {
|
||||
return <StaticImage title={title} icon={icon} className={className} onClick={onClick} />;
|
||||
return <StaticImage title={title} icon={icon} width={width} className={className} onClick={onClick} />;
|
||||
}
|
||||
|
||||
return <Icon name={icon} className={className} viewBox={viewBox || '0 0 32 32'} onClick={onClick} />;
|
||||
return <Icon name={icon} className={className} viewBox={viewBox || '0 0 32 32'} width={width} height={width} onClick={onClick} />;
|
||||
};
|
||||
|
||||
@@ -93,11 +93,14 @@
|
||||
font-weight: 500;
|
||||
min-width: 250px;
|
||||
padding: 8px 24px 8px 8px;
|
||||
border-right: 1px solid;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid;
|
||||
}
|
||||
}
|
||||
.listItemDescription {
|
||||
composes: theme-typography--body2 from global;
|
||||
|
||||
+4
-1
@@ -20,8 +20,9 @@ import { useS } from '../../useS';
|
||||
import { RenderField } from './RenderField';
|
||||
|
||||
interface ObjectPropertyFormProps extends ILayoutSizeProps {
|
||||
properties: ObjectPropertyInfo[];
|
||||
properties: ReadonlyArray<ObjectPropertyInfo>;
|
||||
state?: Record<string, any>;
|
||||
defaultState?: Record<string, any>;
|
||||
category?: string | null;
|
||||
editable?: boolean;
|
||||
autofillToken?: string;
|
||||
@@ -41,6 +42,7 @@ interface ObjectPropertyFormProps extends ILayoutSizeProps {
|
||||
export const ObjectPropertyInfoForm = observer<ObjectPropertyFormProps>(function ObjectPropertyInfoForm({
|
||||
properties,
|
||||
state,
|
||||
defaultState,
|
||||
category,
|
||||
editable = true,
|
||||
className,
|
||||
@@ -86,6 +88,7 @@ export const ObjectPropertyInfoForm = observer<ObjectPropertyFormProps>(function
|
||||
className={s(sizeStyles, { ...(geLayoutSize ? geLayoutSize(property) : layoutProps) }, className)}
|
||||
property={property}
|
||||
state={state}
|
||||
defaultState={defaultState}
|
||||
editable={editable}
|
||||
autofillToken={autofillToken}
|
||||
disabled={disabled}
|
||||
|
||||
+36
-28
@@ -13,7 +13,7 @@ import { removeMetadataFromBase64 } from '@cloudbeaver/core-utils';
|
||||
import { FieldCheckbox } from '../../FormControls/Checkboxes/FieldCheckbox';
|
||||
import { Combobox } from '../../FormControls/Combobox';
|
||||
import { FormFieldDescription } from '../../FormControls/FormFieldDescription';
|
||||
import { InputField } from '../../FormControls/InputField';
|
||||
import { InputField } from '../../FormControls/InputField/InputField';
|
||||
import { InputFileTextContent } from '../../FormControls/InputFileTextContent';
|
||||
import { isControlPresented } from '../../FormControls/isControlPresented';
|
||||
import { Textarea } from '../../FormControls/Textarea';
|
||||
@@ -26,6 +26,7 @@ const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
|
||||
interface RenderFieldProps {
|
||||
property: ObjectPropertyInfo;
|
||||
state?: Record<string, any>;
|
||||
defaultState?: Record<string, any>;
|
||||
editable?: boolean;
|
||||
autofillToken?: string;
|
||||
disabled?: boolean;
|
||||
@@ -55,6 +56,7 @@ function getValue(value: any, controlType: ControlType) {
|
||||
export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
property,
|
||||
state,
|
||||
defaultState,
|
||||
editable = true,
|
||||
autofillToken = '',
|
||||
disabled,
|
||||
@@ -74,26 +76,6 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
|
||||
const value = getValue(property.value, controlType);
|
||||
const defaultValue = getValue(property.defaultValue, controlType);
|
||||
const passwordSaved = showRememberTip && ((password && !!property.value) || saved);
|
||||
const description = passwordSaved ? translate('ui_processing_saved') : undefined;
|
||||
|
||||
if (controlType === 'file' && state) {
|
||||
return (
|
||||
<InputFileTextContent
|
||||
required={required}
|
||||
tooltip={property.description}
|
||||
labelTooltip={property.displayName || property.description}
|
||||
name={property.id!}
|
||||
state={state}
|
||||
disabled={disabled}
|
||||
fileName={description}
|
||||
className={className}
|
||||
mapValue={removeMetadataFromBase64}
|
||||
>
|
||||
{property.displayName}
|
||||
</InputFileTextContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (controlType === 'link') {
|
||||
return (
|
||||
@@ -128,6 +110,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
title={property.description}
|
||||
disabled={disabled || readOnly}
|
||||
className={className}
|
||||
groupGap
|
||||
>
|
||||
{property.displayName ?? ''}
|
||||
</FieldCheckbox>
|
||||
@@ -142,6 +125,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
title={property.description}
|
||||
disabled={disabled || readOnly}
|
||||
className={className}
|
||||
groupGap
|
||||
>
|
||||
{property.displayName ?? ''}
|
||||
</FieldCheckbox>
|
||||
@@ -189,6 +173,27 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
);
|
||||
}
|
||||
|
||||
const passwordSaved = showRememberTip && ((password && !!property.value) || saved);
|
||||
const passwordSavedMessage = passwordSaved ? translate('core_blocks_object_property_info_password_saved') : undefined;
|
||||
|
||||
if (controlType === 'file' && state) {
|
||||
return (
|
||||
<InputFileTextContent
|
||||
required={required}
|
||||
tooltip={property.description}
|
||||
labelTooltip={property.displayName || property.description}
|
||||
name={property.id!}
|
||||
state={state}
|
||||
disabled={disabled}
|
||||
fileName={passwordSavedMessage}
|
||||
className={className}
|
||||
mapValue={removeMetadataFromBase64}
|
||||
>
|
||||
{property.displayName}
|
||||
</InputFileTextContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (controlType === 'textarea') {
|
||||
if (state !== undefined) {
|
||||
return (
|
||||
@@ -196,7 +201,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
required={required}
|
||||
title={state[property.id!]}
|
||||
labelTooltip={property.description || property.displayName}
|
||||
description={description}
|
||||
placeholder={passwordSavedMessage}
|
||||
name={property.id!}
|
||||
state={state}
|
||||
disabled={disabled}
|
||||
@@ -213,7 +218,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
required={required}
|
||||
title={value}
|
||||
labelTooltip={property.description || property.displayName}
|
||||
description={description}
|
||||
placeholder={passwordSavedMessage}
|
||||
name={property.id!}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
@@ -230,15 +235,17 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
<InputField
|
||||
required={required}
|
||||
type={password ? 'password' : 'text'}
|
||||
title={password ? property.description || property.displayName : state[property.id!]}
|
||||
title={password ? property.description || property.displayName : undefined}
|
||||
labelTooltip={property.description || property.displayName}
|
||||
name={property.id!}
|
||||
state={state}
|
||||
defaultState={defaultState}
|
||||
autoHide={autoHide}
|
||||
defaultValue={defaultValue}
|
||||
description={description ?? property.hint}
|
||||
description={property.hint}
|
||||
placeholder={passwordSavedMessage}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
autoHide={autoHide}
|
||||
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
|
||||
className={className}
|
||||
canShowPassword={canShowPassword}
|
||||
@@ -253,12 +260,13 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
|
||||
<InputField
|
||||
required={required}
|
||||
type={password ? 'password' : 'text'}
|
||||
title={password ? property.description || property.displayName : value}
|
||||
title={password ? property.description || property.displayName : undefined}
|
||||
labelTooltip={property.description || property.displayName}
|
||||
name={property.id!}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
description={description ?? property.hint}
|
||||
description={property.hint}
|
||||
placeholder={passwordSavedMessage}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
|
||||
|
||||
@@ -13,18 +13,18 @@ import { Button } from '../Button';
|
||||
import ButtonStyles from '../Button.m.css';
|
||||
import { Filter } from '../FormControls/Filter';
|
||||
import FilterStyles from '../FormControls/Filter.m.css';
|
||||
import InputFieldStyles from '../FormControls/InputField.m.css';
|
||||
import InputFieldStyles from '../FormControls/InputField/InputField.m.css';
|
||||
import { useTranslate } from '../localization/useTranslate';
|
||||
import { s } from '../s';
|
||||
import { SContext, StyleRegistry } from '../SContext';
|
||||
import { useObjectRef } from '../useObjectRef';
|
||||
import { useS } from '../useS';
|
||||
import type { IProperty } from './IProperty';
|
||||
import styles from './PropertiesTable.m.css';
|
||||
import PropertiesTableAddButtonStyles from './PropertiesTableAddButtonStyles.m.css';
|
||||
import PropertiesTableFilterStyles from './PropertiesTableFilterStyles.m.css';
|
||||
import PropertiesTableInputStyles from './PropertiesTableInputStyles.m.css';
|
||||
import { PropertyItem } from './PropertyItem';
|
||||
import { useS } from '../useS';
|
||||
import styles from './PropertiesTable.m.css';
|
||||
import { s } from '../s';
|
||||
|
||||
type PropertiesState = Record<string, string | null>;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ interface IActions<TResource extends IResource<any, any, any, any>, TKey, TInclu
|
||||
active?: boolean;
|
||||
forceSuspense?: boolean;
|
||||
silent?: boolean;
|
||||
onData?: (data: ResourceData<TResource, TKey, TIncludes>, resource: TResource) => Promise<any> | any;
|
||||
onData?: (data: ResourceData<TResource, TKey, TIncludes>, resource: TResource) => any;
|
||||
onError?: (exception: Error | Error[] | null) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -155,8 +155,8 @@ export * from './Tags/Tags';
|
||||
export * from './FormControls/Combobox';
|
||||
export * from './FormControls/FormContext';
|
||||
export * from './FormControls/FormFieldDescription';
|
||||
export * from './FormControls/InputField';
|
||||
export { default as InputFieldStyles } from './FormControls/InputField.m.css';
|
||||
export * from './FormControls/InputField/InputField';
|
||||
export { default as InputFieldStyles } from './FormControls/InputField/InputField.m.css';
|
||||
export * from './FormControls/InputFiles';
|
||||
export * from './FormControls/InputFileTextContent';
|
||||
export * from './FormControls/Radio';
|
||||
|
||||
@@ -5,6 +5,7 @@ export default [
|
||||
['core_block_properties_table_add', 'Add property'],
|
||||
['core_blocks_properties_table_item_reset', 'Reset to default'],
|
||||
['core_blocks_object_property_info_form_empty_placeholder', 'Properties empty'],
|
||||
['core_blocks_object_property_info_password_saved', 'Field value is saved'],
|
||||
['core_blocks_properties_table_item_remove', 'Remove property'],
|
||||
['combobox_no_results_placeholder', 'No results'],
|
||||
['core_blocks_exception_message_error_title', 'Unable to display content'],
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { schema } from '@cloudbeaver/core-utils';
|
||||
|
||||
export const CONNECTION_INFO_PARAM_SCHEMA = schema
|
||||
.object({
|
||||
projectId: schema.string(),
|
||||
connectionId: schema.string(),
|
||||
})
|
||||
.required()
|
||||
.strict();
|
||||
|
||||
export type IConnectionInfoParams = schema.infer<typeof CONNECTION_INFO_PARAM_SCHEMA>;
|
||||
@@ -11,6 +11,7 @@ import { ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import { CachedMapAllKey, CachedMapResource, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { GraphQLService, SqlDialectInfo } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
|
||||
import type { IConnectionExecutionContextInfo } from './ConnectionExecutionContext/ConnectionExecutionContextResource';
|
||||
import {
|
||||
ConnectionInfoActiveProjectKey,
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
ConnectionInfoResource,
|
||||
isConnectionInfoParamEqual,
|
||||
} from './ConnectionInfoResource';
|
||||
import type { IConnectionInfoParams } from './IConnectionsResource';
|
||||
|
||||
export type ConnectionDialect = SqlDialectInfo;
|
||||
|
||||
|
||||
+1
-1
@@ -22,8 +22,8 @@ import {
|
||||
import { GraphQLService, SqlContextInfo } from '@cloudbeaver/core-sdk';
|
||||
import { flat } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
|
||||
import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from '../ConnectionInfoResource';
|
||||
import type { IConnectionInfoParams } from '../IConnectionsResource';
|
||||
|
||||
export const ConnectionExecutionContextProjectKey = resourceKeyAliasFactory('@connection-folder/project', (projectId: string) => ({ projectId }));
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { TaskScheduler } from '@cloudbeaver/core-executor';
|
||||
import { CachedMapAllKey, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IConnectionInfoParams } from '../IConnectionsResource';
|
||||
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
|
||||
import { ConnectionExecutionContext } from './ConnectionExecutionContext';
|
||||
import { ConnectionExecutionContextResource } from './ConnectionExecutionContextResource';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, makeObservable, observable, runInAction } from 'mobx';
|
||||
import { action, makeObservable, observable, runInAction, toJS } from 'mobx';
|
||||
|
||||
import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
@@ -35,10 +35,11 @@ import {
|
||||
TestConnectionMutation,
|
||||
UserConnectionAuthPropertiesFragment,
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
import { schemaValidationError } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { CONNECTION_INFO_PARAM_SCHEMA, type IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
|
||||
import { ConnectionInfoEventHandler, IConnectionInfoEvent } from './ConnectionInfoEventHandler';
|
||||
import type { DatabaseConnection } from './DatabaseConnection';
|
||||
import type { IConnectionInfoParams } from './IConnectionsResource';
|
||||
|
||||
export type Connection = DatabaseConnection & {
|
||||
authProperties?: UserConnectionAuthPropertiesFragment[];
|
||||
@@ -53,6 +54,7 @@ export type ConnectionInitConfig = Omit<
|
||||
| 'includeCredentialsSaved'
|
||||
| 'includeProperties'
|
||||
| 'includeProviderProperties'
|
||||
| 'includeSharedSecrets'
|
||||
| 'customIncludeOptions'
|
||||
>;
|
||||
export type ConnectionInfoIncludes = Omit<GetUserConnectionsQueryVariables, 'id'>;
|
||||
@@ -577,12 +579,17 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
|
||||
includeCredentialsSaved: false,
|
||||
includeProperties: false,
|
||||
includeProviderProperties: false,
|
||||
includeSharedSecrets: false,
|
||||
customIncludeOptions: false,
|
||||
};
|
||||
}
|
||||
|
||||
protected validateKey(key: IConnectionInfoParams): boolean {
|
||||
return typeof key === 'object' && typeof key.projectId === 'string' && typeof key.connectionId === 'string';
|
||||
const parse = CONNECTION_INFO_PARAM_SCHEMA.safeParse(toJS(key));
|
||||
if (!parse.success) {
|
||||
this.logger.warn(`Invalid resource key ${schemaValidationError(parse.error).toString()}`);
|
||||
}
|
||||
return parse.success;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,14 @@ import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-exec
|
||||
import { ProjectInfo, projectInfoSortByName, ProjectsService } from '@cloudbeaver/core-projects';
|
||||
import { isArraysEqual } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
|
||||
import { Connection, ConnectionInfoResource, createConnectionParam, isConnectionInfoParamEqual } from './ConnectionInfoResource';
|
||||
import { ContainerResource, IStructContainers, ObjectContainer } from './ContainerResource';
|
||||
import type { IConnectionInfoParams } from './IConnectionsResource';
|
||||
|
||||
export interface IRequireConnectionExecutorData {
|
||||
key: IConnectionInfoParams;
|
||||
resetCredentials?: boolean;
|
||||
}
|
||||
|
||||
export interface IConnectionExecutorData {
|
||||
connections: IConnectionInfoParams[];
|
||||
@@ -32,7 +37,7 @@ export class ConnectionsManagerService {
|
||||
get createConnectionProjects(): ProjectInfo[] {
|
||||
return this.projectsService.activeProjects.filter(project => project.canEditDataSources).sort(projectInfoSortByName);
|
||||
}
|
||||
readonly connectionExecutor: IExecutor<IConnectionInfoParams>;
|
||||
readonly connectionExecutor: IExecutor<IRequireConnectionExecutorData>;
|
||||
readonly onDisconnect: IExecutor<IConnectionExecutorData>;
|
||||
readonly onDelete: IExecutor<IConnectionExecutorData>;
|
||||
|
||||
@@ -47,11 +52,11 @@ export class ConnectionsManagerService {
|
||||
) {
|
||||
this.disconnecting = false;
|
||||
|
||||
this.connectionExecutor = new Executor<IConnectionInfoParams>(null, isConnectionInfoParamEqual);
|
||||
this.connectionExecutor = new Executor<IRequireConnectionExecutorData>(null, (a, b) => isConnectionInfoParamEqual(a.key, b.key));
|
||||
this.onDisconnect = new Executor();
|
||||
this.onDelete = new Executor();
|
||||
|
||||
this.connectionExecutor.addHandler(key => connectionInfo.load(key));
|
||||
this.connectionExecutor.addHandler(data => connectionInfo.load(data.key));
|
||||
this.onDelete.before(this.onDisconnect);
|
||||
|
||||
makeObservable(this, {
|
||||
@@ -64,8 +69,8 @@ export class ConnectionsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
async requireConnection(key: IConnectionInfoParams): Promise<Connection | null> {
|
||||
const context = await this.connectionExecutor.execute(key);
|
||||
async requireConnection(key: IConnectionInfoParams, resetCredentials?: boolean): Promise<Connection | null> {
|
||||
const context = await this.connectionExecutor.execute({ key, resetCredentials });
|
||||
const connection = context.getContext(this.connectionContext);
|
||||
|
||||
return connection.connection;
|
||||
|
||||
@@ -12,8 +12,8 @@ import { CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList,
|
||||
import { GraphQLService, NavNodeInfoFragment } from '@cloudbeaver/core-sdk';
|
||||
import { isNull } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from './ConnectionInfoResource';
|
||||
import type { IConnectionInfoParams } from './IConnectionsResource';
|
||||
import type { IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
|
||||
import { ConnectionInfoActiveProjectKey, ConnectionInfoResource, createConnectionParam } from './ConnectionInfoResource';
|
||||
|
||||
export type ObjectContainer = NavNodeInfoFragment;
|
||||
export interface ICatalogData {
|
||||
@@ -47,7 +47,14 @@ export class ContainerResource extends CachedMapResource<ObjectContainerParams,
|
||||
|
||||
appAuthService.requireAuthentication(this);
|
||||
this.preloadResource(connectionInfoResource, () => ConnectionInfoActiveProjectKey);
|
||||
this.before(ExecutorInterrupter.interrupter(key => !connectionInfoResource.isConnected(key)));
|
||||
this.before(
|
||||
ExecutorInterrupter.interrupter(key => {
|
||||
if (isResourceAlias(key)) {
|
||||
return false;
|
||||
}
|
||||
return !connectionInfoResource.isConnected(ResourceKeyUtils.mapKey(key, key => createConnectionParam(key.projectId, key.connectionId)));
|
||||
}),
|
||||
);
|
||||
|
||||
this.connectionInfoResource.onItemDelete.addHandler(key =>
|
||||
ResourceKeyUtils.forEach(key, key => this.delete({ projectId: key.projectId, connectionId: key.connectionId })),
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AppAuthService } from '@cloudbeaver/core-authentication';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { CachedMapAllKey, CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import { DatabaseDriverFragment, DriverListQueryVariables, GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { DatabaseDriverFragment, DriverConfigurationType, DriverListQueryVariables, GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { isArraysEqual } from '@cloudbeaver/core-utils';
|
||||
|
||||
export type DBDriver = DatabaseDriverFragment;
|
||||
@@ -19,10 +19,10 @@ export type DBDriver = DatabaseDriverFragment;
|
||||
export const NEW_DRIVER_SYMBOL = Symbol('new-driver');
|
||||
|
||||
export type NewDBDriver = DBDriver & { [NEW_DRIVER_SYMBOL]: boolean; timestamp: number };
|
||||
type DriverResourceIncludes = Omit<DriverListQueryVariables, 'driverId'>;
|
||||
export type DBDriverResourceIncludes = Omit<DriverListQueryVariables, 'driverId'>;
|
||||
|
||||
@injectable()
|
||||
export class DBDriverResource extends CachedMapResource<string, DBDriver, DriverListQueryVariables> {
|
||||
export class DBDriverResource extends CachedMapResource<string, DBDriver, DBDriverResourceIncludes> {
|
||||
get enabledDrivers() {
|
||||
return this.values.filter(driver => driver.enabled).sort(this.compare);
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export class DBDriverResource extends CachedMapResource<string, DBDriver, Driver
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultIncludes(): DriverResourceIncludes {
|
||||
getDefaultIncludes(): DBDriverResourceIncludes {
|
||||
return {
|
||||
includeDriverLibraries: false,
|
||||
includeDriverParameters: false,
|
||||
@@ -140,3 +140,19 @@ export function compareNewDrivers(a: DBDriver, b: DBDriver): number {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getDBDriverDefaultAuthModelId(driver: DBDriver | undefined): string | undefined {
|
||||
return driver?.defaultAuthModel || driver?.applicableAuthModels[0];
|
||||
}
|
||||
|
||||
export function getDBDriverDefaultConfigurationType(driver: DBDriver | undefined): DriverConfigurationType {
|
||||
return driver?.configurationTypes[0] || DriverConfigurationType.Manual;
|
||||
}
|
||||
|
||||
export function getDBDriverDefaultServerName(driver: DBDriver | undefined): string | undefined {
|
||||
return driver?.requiresServerName ? '' : undefined;
|
||||
}
|
||||
|
||||
export function getDBDriverDefaultServer(driver: DBDriver | undefined): string | undefined {
|
||||
return driver?.defaultServer || 'localhost';
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { CachedMapAllKey, CachedMapResource, resourceKeyList } from '@cloudbeaver/core-resource';
|
||||
import { ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import { type DatabaseAuthModel, GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { type DatabaseAuthModel as DatabaseAuthModelBase, GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
|
||||
export type DatabaseAuthModel = DatabaseAuthModelBase;
|
||||
|
||||
@injectable()
|
||||
export class DatabaseAuthModelsResource extends CachedMapResource<string, DatabaseAuthModel> {
|
||||
|
||||
@@ -20,11 +20,11 @@ import { getProjectNodeId } from '@cloudbeaver/core-projects';
|
||||
import { isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { ServerEventId } from '@cloudbeaver/core-root';
|
||||
|
||||
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
|
||||
import { ConnectionFolderEventHandler, IConnectionFolderEvent } from '../ConnectionFolderEventHandler';
|
||||
import { Connection, ConnectionInfoActiveProjectKey, ConnectionInfoResource, createConnectionParam } from '../ConnectionInfoResource';
|
||||
import { ConnectionsManagerService } from '../ConnectionsManagerService';
|
||||
import { ContainerResource } from '../ContainerResource';
|
||||
import type { IConnectionInfoParams } from '../IConnectionsResource';
|
||||
import { getConnectionParentId } from './getConnectionParentId';
|
||||
import { getFolderNodeParents } from './getFolderNodeParents';
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import type { IExtension } from '@cloudbeaver/core-extensions';
|
||||
import { NavNodeInfoResource, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
|
||||
import { projectProvider } from '@cloudbeaver/core-projects';
|
||||
|
||||
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
|
||||
import { ConnectionInfoResource, createConnectionParam } from '../ConnectionInfoResource';
|
||||
import { connectionProvider } from '../extensions/IConnectionProvider';
|
||||
import { objectCatalogProvider } from '../extensions/IObjectCatalogProvider';
|
||||
import { objectSchemaProvider } from '../extensions/IObjectSchemaProvider';
|
||||
import type { IConnectionInfoParams } from '../IConnectionsResource';
|
||||
|
||||
@injectable()
|
||||
export class NavNodeExtensionsService {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { createExtension, IExtension, isExtension } from '@cloudbeaver/core-extensions';
|
||||
|
||||
import type { IConnectionInfoParams } from '../IConnectionsResource';
|
||||
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
|
||||
|
||||
const connectionProviderSymbol = Symbol('@extension/ConnectionProvider');
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { createExtension, IExtension, isExtension } from '@cloudbeaver/core-extensions';
|
||||
|
||||
import type { IConnectionInfoParams } from '../IConnectionsResource';
|
||||
import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA';
|
||||
|
||||
const connectionSetterSymbol = Symbol('@extension/ConnectionSetter');
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export * from './createConnectionFolderParam';
|
||||
export * from './DatabaseAuthModelsResource';
|
||||
export * from './DatabaseConnection';
|
||||
export * from './DBDriverResource';
|
||||
export * from './IConnectionsResource';
|
||||
export * from './CONNECTION_INFO_PARAM_SCHEMA';
|
||||
export * from './isJDBCConnection';
|
||||
export * from './NetworkHandlerResource';
|
||||
export * from './useConnectionInfo';
|
||||
|
||||
@@ -10,9 +10,9 @@ import { computed, observable } from 'mobx';
|
||||
import { useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import type { IConnectionInfoParams } from './CONNECTION_INFO_PARAM_SCHEMA';
|
||||
import { Connection, ConnectionInfoResource } from './ConnectionInfoResource';
|
||||
import { ConnectionsManagerService } from './ConnectionsManagerService';
|
||||
import type { IConnectionInfoParams } from './IConnectionsResource';
|
||||
|
||||
interface IPrivate extends IPublic {
|
||||
manager: ConnectionsManagerService;
|
||||
|
||||
@@ -78,6 +78,7 @@ export default [
|
||||
['ui_reconnect', 'Reconnect'],
|
||||
['ui_forget', 'Forget'],
|
||||
['ui_set', 'Set'],
|
||||
['ui_restore', 'Restore'],
|
||||
['ui_are_you_sure', 'Are you sure?'],
|
||||
['ui_options', 'Options'],
|
||||
['ui_changes_might_be_lost', 'Your changes might be lost'],
|
||||
|
||||
@@ -69,6 +69,13 @@ export default [
|
||||
['ui_no_items_placeholder', 'Non ci sono ancora elementi.'],
|
||||
['ui_search_no_result_placeholder', 'Nessun risultato trovato.'],
|
||||
['ui_save_reminder', 'Ci sono modifiche non salvate.'],
|
||||
['ui_yes', 'Yes'],
|
||||
['ui_no', 'No'],
|
||||
['ui_select_all', 'Select all'],
|
||||
['ui_reconnect', 'Reconnect'],
|
||||
['ui_forget', 'Forget'],
|
||||
['ui_set', 'Set'],
|
||||
['ui_restore', 'Restore'],
|
||||
['ui_settings', 'Impostazioni'],
|
||||
['ui_advanced_settings', 'Advanced settings'],
|
||||
['ui_expand', 'Expand'],
|
||||
|
||||
@@ -74,6 +74,7 @@ export default [
|
||||
['ui_reconnect', 'Переподключиться'],
|
||||
['ui_forget', 'Забыть'],
|
||||
['ui_set', 'Установить'],
|
||||
['ui_restore', 'Восстановить'],
|
||||
['ui_are_you_sure', 'Вы уверены?'],
|
||||
['ui_options', 'Параметры'],
|
||||
['ui_changes_might_be_lost', 'Ваши изменения могут быть потеряны'],
|
||||
|
||||
@@ -75,6 +75,7 @@ export default [
|
||||
['ui_reconnect', '重新连接'],
|
||||
['ui_forget', '忘记'],
|
||||
['ui_set', '设置'],
|
||||
['ui_restore', 'Restore'],
|
||||
['ui_are_you_sure', '您确定吗?'],
|
||||
['ui_options', '选项'],
|
||||
['ui_changes_might_be_lost', '您的更改可能会丢失'],
|
||||
|
||||
@@ -58,32 +58,32 @@ export abstract class CachedDataResource<
|
||||
|
||||
export function getCachedDataResourceLoaderState<TData, TKey = void, TContext extends Record<string, any> = Record<string, never>>(
|
||||
resource: CachedDataResource<TData, TKey, TContext>,
|
||||
param: TKey,
|
||||
context?: CachedResourceIncludeArgs<TData, TContext>,
|
||||
getKey: () => TKey,
|
||||
getIncludes?: () => CachedResourceIncludeArgs<TData, TContext> | undefined,
|
||||
lazy?: boolean,
|
||||
): ILoadableState {
|
||||
return {
|
||||
lazy,
|
||||
get exception() {
|
||||
return resource.getException(param);
|
||||
return resource.getException(getKey());
|
||||
},
|
||||
isLoading() {
|
||||
return resource.isLoading(param);
|
||||
return resource.isLoading(getKey());
|
||||
},
|
||||
isError() {
|
||||
return isContainsException(this.exception);
|
||||
},
|
||||
isLoaded() {
|
||||
return resource.isLoaded(param, context);
|
||||
return resource.isLoaded(getKey(), getIncludes?.());
|
||||
},
|
||||
isOutdated() {
|
||||
return resource.isOutdated(param, context);
|
||||
return resource.isOutdated(getKey(), getIncludes?.());
|
||||
},
|
||||
async load() {
|
||||
await resource.load(param, context);
|
||||
await resource.load(getKey(), getIncludes?.());
|
||||
},
|
||||
async reload() {
|
||||
await resource.refresh(param, context);
|
||||
await resource.refresh(getKey(), getIncludes?.());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -265,32 +265,68 @@ export abstract class CachedMapResource<
|
||||
|
||||
export function getCachedMapResourceLoaderState<TKey, TValue, TContext extends Record<string, any> = Record<string, never>>(
|
||||
resource: CachedMapResource<TKey, TValue, TContext>,
|
||||
getKey: () => ResourceKey<TKey>,
|
||||
getKey: () => ResourceKey<TKey> | null,
|
||||
getIncludes?: () => CachedResourceIncludeArgs<TValue, TContext> | undefined,
|
||||
lazy?: boolean,
|
||||
): ILoadableState {
|
||||
return {
|
||||
lazy,
|
||||
get exception() {
|
||||
return resource.getException(getKey());
|
||||
const key = getKey();
|
||||
|
||||
if (key === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resource.getException(key);
|
||||
},
|
||||
isLoading() {
|
||||
return resource.isLoading(getKey());
|
||||
const key = getKey();
|
||||
|
||||
if (key === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return resource.isLoading(key);
|
||||
},
|
||||
isLoaded() {
|
||||
return resource.isLoaded(getKey(), getIncludes?.());
|
||||
const key = getKey();
|
||||
|
||||
if (key === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return resource.isLoaded(key, getIncludes?.());
|
||||
},
|
||||
isError() {
|
||||
return isContainsException(this.exception);
|
||||
},
|
||||
isOutdated() {
|
||||
return resource.isOutdated(getKey(), getIncludes?.());
|
||||
const key = getKey();
|
||||
|
||||
if (key === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return resource.isOutdated(key, getIncludes?.());
|
||||
},
|
||||
async load() {
|
||||
await resource.load(getKey(), getIncludes?.());
|
||||
const key = getKey();
|
||||
|
||||
if (key === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await resource.load(key, getIncludes?.());
|
||||
},
|
||||
async reload() {
|
||||
await resource.refresh(getKey(), getIncludes?.());
|
||||
const key = getKey();
|
||||
|
||||
if (key === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await resource.refresh(key, getIncludes?.());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -655,10 +655,14 @@ export abstract class CachedResource<
|
||||
this.dataUpdate(key);
|
||||
}
|
||||
},
|
||||
error: exception => {
|
||||
this.markOutdatedSync(key);
|
||||
this.markError(exception, key, include);
|
||||
},
|
||||
// TODO: must rethink how to handle exceptions form performUpdate
|
||||
// probably we need to handle this exceptions at the place
|
||||
// where performUpdate is called
|
||||
// because performUpdate is like an transaction
|
||||
// error: exception => {
|
||||
// this.markOutdatedSync(key);
|
||||
// this.markError(exception, key, include);
|
||||
// },
|
||||
after: () => {
|
||||
this.flushOutdatedWaitList();
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mutation closeConnection(
|
||||
$projectId: ID!
|
||||
$connectionId: ID!
|
||||
|
||||
$includeOrigin: Boolean!
|
||||
$customIncludeOriginDetails: Boolean!
|
||||
$includeAuthProperties: Boolean!
|
||||
@@ -10,9 +9,10 @@ mutation closeConnection(
|
||||
$includeAuthNeeded: Boolean!
|
||||
$includeProperties: Boolean!
|
||||
$includeProviderProperties: Boolean!
|
||||
$includeSharedSecrets: Boolean!
|
||||
$customIncludeOptions: Boolean!
|
||||
) {
|
||||
connection: closeConnection(projectId: $projectId, id: $connectionId) {
|
||||
...DatabaseConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mutation createConnection(
|
||||
$projectId: ID!
|
||||
$config: ConnectionConfig!
|
||||
|
||||
$includeOrigin: Boolean!
|
||||
$customIncludeOriginDetails: Boolean!
|
||||
$includeAuthProperties: Boolean!
|
||||
@@ -10,6 +9,7 @@ mutation createConnection(
|
||||
$includeAuthNeeded: Boolean!
|
||||
$includeProperties: Boolean!
|
||||
$includeProviderProperties: Boolean!
|
||||
$includeSharedSecrets: Boolean!
|
||||
$customIncludeOptions: Boolean!
|
||||
) {
|
||||
connection: createConnection(projectId: $projectId, config: $config) {
|
||||
|
||||
@@ -2,7 +2,6 @@ mutation createConnectionFromNode(
|
||||
$projectId: ID!
|
||||
$nodePath: String!
|
||||
$config: ConnectionConfig
|
||||
|
||||
$includeOrigin: Boolean!
|
||||
$customIncludeOriginDetails: Boolean!
|
||||
$includeAuthProperties: Boolean!
|
||||
@@ -11,6 +10,7 @@ mutation createConnectionFromNode(
|
||||
$includeAuthNeeded: Boolean!
|
||||
$includeProperties: Boolean!
|
||||
$includeProviderProperties: Boolean!
|
||||
$includeSharedSecrets: Boolean!
|
||||
$customIncludeOptions: Boolean!
|
||||
) {
|
||||
connection: copyConnectionFromNode(projectId: $projectId, nodePath: $nodePath, config: $config) {
|
||||
|
||||
@@ -2,7 +2,6 @@ mutation createConnectionFromTemplate(
|
||||
$projectId: ID!
|
||||
$templateId: ID!
|
||||
$connectionName: String!
|
||||
|
||||
$includeOrigin: Boolean!
|
||||
$customIncludeOriginDetails: Boolean!
|
||||
$includeAuthProperties: Boolean!
|
||||
@@ -11,6 +10,7 @@ mutation createConnectionFromTemplate(
|
||||
$includeAuthNeeded: Boolean!
|
||||
$includeProperties: Boolean!
|
||||
$includeProviderProperties: Boolean!
|
||||
$includeSharedSecrets: Boolean!
|
||||
$customIncludeOptions: Boolean!
|
||||
) {
|
||||
connection: createConnectionFromTemplate(projectId: $projectId, templateId: $templateId, connectionName: $connectionName) {
|
||||
|
||||
@@ -8,9 +8,10 @@ query getTemplateConnections(
|
||||
$includeAuthNeeded: Boolean!
|
||||
$includeProperties: Boolean!
|
||||
$includeProviderProperties: Boolean!
|
||||
$includeSharedSecrets: Boolean!
|
||||
$customIncludeOptions: Boolean!
|
||||
) {
|
||||
connections: templateConnections(projectId: $projectId) {
|
||||
...DatabaseConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ query getUserConnections(
|
||||
$includeAuthNeeded: Boolean!
|
||||
$includeProperties: Boolean!
|
||||
$includeProviderProperties: Boolean!
|
||||
$includeSharedSecrets: Boolean!
|
||||
$customIncludeOptions: Boolean!
|
||||
) {
|
||||
connections: userConnections(projectId: $projectId, id: $connectionId, projectIds: $projectIds) {
|
||||
...DatabaseConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ mutation initConnection(
|
||||
$projectId: ID!
|
||||
$connectionId: ID!
|
||||
$credentials: Object
|
||||
$networkCredentials: [NetworkHandlerConfigInput!]
|
||||
$networkCredentials: [NetworkHandlerConfigInput!]
|
||||
$saveCredentials: Boolean
|
||||
$sharedCredentials: Boolean
|
||||
|
||||
$selectedSecretId: String
|
||||
$includeOrigin: Boolean!
|
||||
$customIncludeOriginDetails: Boolean!
|
||||
$includeAuthProperties: Boolean!
|
||||
@@ -14,6 +14,7 @@ mutation initConnection(
|
||||
$includeAuthNeeded: Boolean!
|
||||
$includeProperties: Boolean!
|
||||
$includeProviderProperties: Boolean!
|
||||
$includeSharedSecrets: Boolean!
|
||||
$customIncludeOptions: Boolean!
|
||||
) {
|
||||
connection: initConnection(
|
||||
@@ -21,8 +22,9 @@ mutation initConnection(
|
||||
id: $connectionId
|
||||
credentials: $credentials
|
||||
networkCredentials: $networkCredentials
|
||||
saveCredentials: $saveCredentials,
|
||||
sharedCredentials: $sharedCredentials,
|
||||
saveCredentials: $saveCredentials
|
||||
sharedCredentials: $sharedCredentials
|
||||
selectedSecretId: $selectedSecretId
|
||||
) {
|
||||
...DatabaseConnection
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ mutation setConnectionNavigatorSettings(
|
||||
$projectId: ID!
|
||||
$connectionId: ID!
|
||||
$settings: NavigatorSettingsInput!
|
||||
|
||||
$includeOrigin: Boolean!
|
||||
$customIncludeOriginDetails: Boolean!
|
||||
$includeAuthProperties: Boolean!
|
||||
@@ -11,6 +10,7 @@ mutation setConnectionNavigatorSettings(
|
||||
$includeAuthNeeded: Boolean!
|
||||
$includeProperties: Boolean!
|
||||
$includeProviderProperties: Boolean!
|
||||
$includeSharedSecrets: Boolean!
|
||||
$customIncludeOptions: Boolean!
|
||||
) {
|
||||
connection: setConnectionNavigatorSettings(projectId: $projectId, id: $connectionId, settings: $settings) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mutation updateConnection(
|
||||
$projectId: ID!
|
||||
$config: ConnectionConfig!
|
||||
|
||||
$includeOrigin: Boolean!
|
||||
$customIncludeOriginDetails: Boolean!
|
||||
$includeAuthProperties: Boolean!
|
||||
@@ -10,6 +9,7 @@ mutation updateConnection(
|
||||
$includeAuthNeeded: Boolean!
|
||||
$includeProperties: Boolean!
|
||||
$includeProviderProperties: Boolean!
|
||||
$includeSharedSecrets: Boolean!
|
||||
$customIncludeOptions: Boolean!
|
||||
) {
|
||||
connection: updateConnection(projectId: $projectId, config: $config) {
|
||||
|
||||
@@ -37,6 +37,10 @@ fragment DatabaseConnection on ConnectionInfo {
|
||||
...ObjectOriginInfo
|
||||
}
|
||||
|
||||
sharedSecrets @include(if: $includeSharedSecrets) {
|
||||
displayName
|
||||
secretId
|
||||
}
|
||||
authNeeded @include(if: $includeAuthNeeded)
|
||||
authModel
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { Tab as BaseTab } from 'reakit/Tab';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useTab } from './useTab';
|
||||
|
||||
export const Tab = observer<TabProps>(function Tab({ tabId, title, menuContext, disabled, className, children, style, onOpen, onClose, onClick }) {
|
||||
const translate = useTranslate();
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const tabContext = useMemo(() => ({ tabId }), [tabId]);
|
||||
const tab = useTab(tabId, onOpen, onClose, onClick);
|
||||
const info = tab.getInfo();
|
||||
@@ -46,6 +47,7 @@ export const Tab = observer<TabProps>(function Tab({ tabId, title, menuContext,
|
||||
<TabMenu tabId={tabId} state={tab.state} menuContext={menuContext} style={style} />
|
||||
</tab-actions>
|
||||
<BaseTab
|
||||
ref={ref}
|
||||
{...tab.state.state}
|
||||
type="button"
|
||||
title={translate(title ?? info?.title)}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useTabState } from 'reakit/Tab';
|
||||
import { useAutoLoad, useExecutor, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
import { useDataContext } from '@cloudbeaver/core-data-context';
|
||||
import { Executor, ExecutorInterrupter } from '@cloudbeaver/core-executor';
|
||||
import { isDefined, isNull, isUndefined, MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils';
|
||||
import { isDefined, isNotNullDefined, MetadataMap, MetadataValueGetter } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { ITabData, ITabInfo, ITabsContainer } from './TabsContainer/ITabsContainer';
|
||||
import { ITabsContext, type TabDirection, TabsContext } from './TabsContext';
|
||||
@@ -65,12 +65,6 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
displayed = tabList;
|
||||
}
|
||||
|
||||
if (!selectedId && (currentTabId === undefined || currentTabId === null) && autoSelect) {
|
||||
if (displayed.length > 0) {
|
||||
selectedId = displayed[0];
|
||||
}
|
||||
}
|
||||
|
||||
const closable = !!onClose;
|
||||
|
||||
const [localTabsState] = useState(() => new MetadataMap<string, any>());
|
||||
@@ -86,7 +80,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
|
||||
const dynamic = useObjectRef(
|
||||
() => ({
|
||||
selectedId: selectedId || currentTabId,
|
||||
selectedId: state.selectedId,
|
||||
}),
|
||||
{
|
||||
canClose,
|
||||
@@ -100,30 +94,22 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
},
|
||||
);
|
||||
|
||||
if ((!isNull(currentTabId) && !isUndefined(currentTabId)) || !autoSelect) {
|
||||
if (isNotNullDefined(currentTabId)) {
|
||||
state.selectedId = currentTabId;
|
||||
dynamic.selectedId = currentTabId;
|
||||
}
|
||||
|
||||
if (
|
||||
displayed.length > 0 &&
|
||||
!isNull(dynamic.selectedId) &&
|
||||
!isUndefined(dynamic.selectedId) &&
|
||||
!isNull(selectedId) &&
|
||||
!isUndefined(selectedId) &&
|
||||
autoSelect
|
||||
) {
|
||||
const tabExists = displayed.includes(dynamic.selectedId);
|
||||
if (displayed.length > 0 && autoSelect) {
|
||||
const tabExists = isNotNullDefined(state.selectedId) && displayed.includes(state.selectedId);
|
||||
|
||||
if (!tabExists) {
|
||||
if (displayed.includes(selectedId)) {
|
||||
state.selectedId = selectedId;
|
||||
} else {
|
||||
state.selectedId = displayed[0];
|
||||
}
|
||||
state.selectedId = displayed[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (isNotNullDefined(currentTabId)) {
|
||||
dynamic.selectedId = state.selectedId;
|
||||
}
|
||||
|
||||
useExecutor({
|
||||
executor: openExecutor,
|
||||
handlers: [
|
||||
@@ -134,7 +120,10 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
return;
|
||||
}
|
||||
dynamic.selectedId = data.tabId;
|
||||
dynamic.state.setSelectedId(data.tabId);
|
||||
if (dynamic.state.selectedId !== data.tabId) {
|
||||
dynamic.state.setCurrentId(data.tabId);
|
||||
dynamic.state.setSelectedId(data.tabId);
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -148,25 +137,18 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
|
||||
],
|
||||
});
|
||||
|
||||
const currentSelectedId = state.selectedId;
|
||||
|
||||
useEffect(() => {
|
||||
if ((!isNull(currentTabId) && !isUndefined(currentTabId)) || !autoSelect) {
|
||||
if (!isNotNullDefined(currentSelectedId) || dynamic.selectedId === currentSelectedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
openExecutor.execute({
|
||||
tabId: state.selectedId!,
|
||||
tabId: currentSelectedId,
|
||||
props,
|
||||
});
|
||||
}, [currentTabId, state.selectedId, autoSelect]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNull(state.selectedId) && !isUndefined(state.selectedId)) {
|
||||
openExecutor.execute({
|
||||
tabId: state.selectedId,
|
||||
props,
|
||||
});
|
||||
}
|
||||
}, [!isNull(state.selectedId) && !isUndefined(state.selectedId)]);
|
||||
}, [currentSelectedId]);
|
||||
|
||||
const value = useObservableRef<ITabsContext<T>>(
|
||||
() => ({
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const EMPTY_ARRAY: ReadonlyArray<any> = Object.freeze([]);
|
||||
export const EMPTY_OBJECT: Readonly<Record<any, any>> = Object.freeze({});
|
||||
@@ -8,6 +8,7 @@ export * from './Quadtree/index';
|
||||
|
||||
export * from './underscore';
|
||||
|
||||
export * from './emptyConstants';
|
||||
export * from './base64ToBlob';
|
||||
export * from './blobToBase64';
|
||||
export * from './base64ToHex';
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@ export const ServerConfigurationInfoForm = observer<Props>(function ServerConfig
|
||||
name="sessionExpireTime"
|
||||
state={state.serverConfig}
|
||||
min={1}
|
||||
mapState={v => (v === 0 ? 60000 : v ?? 1800000) / 1000 / 60}
|
||||
mapValue={v => (v === undefined ? 30 : Number(v) || 1) * 1000 * 60}
|
||||
mapState={(v: number | undefined) => String((v === 0 ? 60000 : v ?? 1800000) / 1000 / 60)}
|
||||
mapValue={(v?: string) => (v === undefined ? 30 : Number(v) || 1) * 1000 * 60}
|
||||
required
|
||||
tiny
|
||||
>
|
||||
|
||||
+2
-2
@@ -143,8 +143,8 @@ export class UserFormInfoPart extends FormPart<IUserFormInfoState, IUserFormStat
|
||||
const loadableStateContext = this.formState.dataContext.get(DATA_CONTEXT_LOADABLE_STATE);
|
||||
|
||||
loadableStateContext.getState('user-info', () => [
|
||||
getCachedDataResourceLoaderState(this.serverConfigResource, undefined),
|
||||
getCachedDataResourceLoaderState(this.authRolesResource, undefined),
|
||||
getCachedDataResourceLoaderState(this.serverConfigResource, () => undefined),
|
||||
getCachedDataResourceLoaderState(this.authRolesResource, () => undefined),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
+12
-8
@@ -6,6 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { AppAuthService } from '@cloudbeaver/core-authentication';
|
||||
import { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
|
||||
import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
|
||||
@@ -14,10 +15,11 @@ import { ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core
|
||||
import { MENU_CONNECTIONS } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
import { ACTION_CONNECTION_TEMPLATE } from './Actions/ACTION_CONNECTION_TEMPLATE';
|
||||
import { ConnectionDialog } from './ConnectionDialog/ConnectionDialog';
|
||||
import { TemplateConnectionsResource } from './TemplateConnectionsResource';
|
||||
import { TemplateConnectionsService } from './TemplateConnectionsService';
|
||||
|
||||
const ConnectionDialog = importLazyComponent(() => import('./ConnectionDialog/ConnectionDialog').then(m => m.ConnectionDialog));
|
||||
|
||||
@injectable()
|
||||
export class TemplateConnectionPluginBootstrap extends Bootstrap {
|
||||
constructor(
|
||||
@@ -46,13 +48,15 @@ export class TemplateConnectionPluginBootstrap extends Bootstrap {
|
||||
!this.appAuthService.authenticated ||
|
||||
!this.projectsService.userProject?.canEditDataSources ||
|
||||
!this.templateConnectionsService.projectTemplates.length,
|
||||
getLoader: (context, action) => {
|
||||
return [
|
||||
...this.appAuthService.loaders,
|
||||
getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey),
|
||||
getCachedDataResourceLoaderState(this.templateConnectionsResource, undefined, undefined),
|
||||
];
|
||||
},
|
||||
getLoader: () => [
|
||||
...this.appAuthService.loaders,
|
||||
getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey),
|
||||
getCachedDataResourceLoaderState(
|
||||
this.templateConnectionsResource,
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
),
|
||||
],
|
||||
handler: async (context, action) => {
|
||||
switch (action) {
|
||||
case ACTION_CONNECTION_TEMPLATE: {
|
||||
|
||||
@@ -68,6 +68,7 @@ export class TemplateConnectionsResource extends CachedDataResource<Connection[]
|
||||
includeCredentialsSaved: false,
|
||||
includeProperties: false,
|
||||
includeProviderProperties: false,
|
||||
includeSharedSecrets: false,
|
||||
customIncludeOptions: false,
|
||||
});
|
||||
return connections;
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import { ConnectionInfoResource, createConnectionParam, IConnectionInfoParams }
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { executorHandlerFilter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { isGlobalProject, ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { PermissionsService, EAdminPermission } from '@cloudbeaver/core-root';
|
||||
import { EAdminPermission, PermissionsService } from '@cloudbeaver/core-root';
|
||||
import { formStateContext } from '@cloudbeaver/core-ui';
|
||||
import type { MetadataValueGetter } from '@cloudbeaver/core-utils';
|
||||
import {
|
||||
|
||||
@@ -6,20 +6,21 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { AuthProviderService } from '@cloudbeaver/core-authentication';
|
||||
import { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
Connection,
|
||||
ConnectionInfoResource,
|
||||
ConnectionsManagerService,
|
||||
createConnectionParam,
|
||||
IConnectionInfoParams,
|
||||
IRequireConnectionExecutorData,
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { Dependency, injectable } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import { AuthenticationService } from '@cloudbeaver/plugin-authentication';
|
||||
|
||||
import { DatabaseAuthDialog } from './DatabaseAuthDialog/DatabaseAuthDialog';
|
||||
const DatabaseAuthDialog = importLazyComponent(() => import('./DatabaseAuthDialog/DatabaseAuthDialog').then(m => m.DatabaseAuthDialog));
|
||||
|
||||
@injectable()
|
||||
export class ConnectionAuthService extends Dependency {
|
||||
@@ -28,7 +29,6 @@ export class ConnectionAuthService extends Dependency {
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
private readonly authProviderService: AuthProviderService,
|
||||
private readonly connectionsManagerService: ConnectionsManagerService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly authenticationService: AuthenticationService,
|
||||
) {
|
||||
super();
|
||||
@@ -40,10 +40,10 @@ export class ConnectionAuthService extends Dependency {
|
||||
}));
|
||||
}
|
||||
|
||||
private async connectionDialog(connectionKey: IConnectionInfoParams, context: IExecutionContextProvider<IConnectionInfoParams | null>) {
|
||||
private async connectionDialog(data: IRequireConnectionExecutorData, context: IExecutionContextProvider<IRequireConnectionExecutorData | null>) {
|
||||
const connection = context.getContext(this.connectionsManagerService.connectionContext);
|
||||
|
||||
const newConnection = await this.auth(connectionKey);
|
||||
const newConnection = await this.auth(data.key, data.resetCredentials);
|
||||
|
||||
if (!newConnection?.connected) {
|
||||
return;
|
||||
@@ -51,7 +51,7 @@ export class ConnectionAuthService extends Dependency {
|
||||
connection.connection = newConnection;
|
||||
}
|
||||
|
||||
async auth(key: IConnectionInfoParams, resetCredentials?: boolean): Promise<Connection | null> {
|
||||
private async auth(key: IConnectionInfoParams, resetCredentials?: boolean): Promise<Connection | null> {
|
||||
if (!this.connectionInfoResource.has(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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 { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
|
||||
export const ConnectionAuthenticationDialogLoader = importLazyComponent(() =>
|
||||
import('./ConnectionAuthenticationDialog').then(module => module.ConnectionAuthenticationDialog),
|
||||
);
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
export const ConnectionAuthenticationDialogLoader = React.lazy(async () => {
|
||||
const { ConnectionAuthenticationDialog } = await import('./ConnectionAuthenticationDialog');
|
||||
return { default: ConnectionAuthenticationDialog };
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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 { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
|
||||
export const ConnectionAuthenticationFormLoader = importLazyComponent(() =>
|
||||
import('./ConnectionAuthenticationForm').then(m => m.ConnectionAuthenticationForm),
|
||||
);
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
export const ConnectionAuthenticationFormLoader = React.lazy(async () => {
|
||||
const { ConnectionAuthenticationForm } = await import('./ConnectionAuthenticationForm');
|
||||
return { default: ConnectionAuthenticationForm };
|
||||
});
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 { Container, ObjectPropertyInfoForm } from '@cloudbeaver/core-blocks';
|
||||
import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
|
||||
import { isSafari } from '@cloudbeaver/core-utils';
|
||||
|
||||
interface Props {
|
||||
credentials?: Record<string, any>;
|
||||
defaultCredentials?: Record<string, any>;
|
||||
properties: ReadonlyArray<ObjectPropertyInfo>;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ConnectionAuthModelCredentialsForm = observer<Props>(function ConnectionAuthModelCredentialsForm({
|
||||
credentials,
|
||||
defaultCredentials,
|
||||
properties,
|
||||
readonly,
|
||||
disabled,
|
||||
}) {
|
||||
return (
|
||||
<Container wrap gap hideEmpty>
|
||||
<ObjectPropertyInfoForm
|
||||
autofillToken={isSafari ? 'section-connection-authentication section-secrets' : 'new-password'}
|
||||
properties={properties}
|
||||
state={credentials}
|
||||
defaultState={defaultCredentials}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
showRememberTip
|
||||
hideEmptyPlaceholder
|
||||
tiny
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 { Combobox, usePermission, useResource } from '@cloudbeaver/core-blocks';
|
||||
import { DatabaseAuthModelsResource } from '@cloudbeaver/core-connections';
|
||||
import { CachedResourceListEmptyKey, resourceKeyList } from '@cloudbeaver/core-resource';
|
||||
import { EAdminPermission } from '@cloudbeaver/core-root';
|
||||
import { isNotNullDefined } from '@cloudbeaver/core-utils';
|
||||
|
||||
interface Props {
|
||||
authModelCredentialsState: { authModelId?: string };
|
||||
applicableAuthModels: string[];
|
||||
readonlyAuthModelId?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
onAuthModelChange?: (authModelId: string | undefined) => void;
|
||||
}
|
||||
|
||||
export const ConnectionAuthModelSelector = observer<Props>(function ConnectionAuthModelSelector({
|
||||
authModelCredentialsState,
|
||||
applicableAuthModels,
|
||||
onAuthModelChange,
|
||||
readonlyAuthModelId,
|
||||
readonly,
|
||||
disabled,
|
||||
}) {
|
||||
const adminPermission = usePermission(EAdminPermission.admin);
|
||||
|
||||
const authModelsLoader = useResource(
|
||||
ConnectionAuthModelSelector,
|
||||
DatabaseAuthModelsResource,
|
||||
applicableAuthModels.length ? resourceKeyList(applicableAuthModels) : CachedResourceListEmptyKey,
|
||||
);
|
||||
|
||||
const availableAuthModels = authModelsLoader.data.filter(isNotNullDefined).filter(model => adminPermission || !model.requiresLocalConfiguration);
|
||||
|
||||
if (availableAuthModels.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
name="authModelId"
|
||||
state={authModelCredentialsState}
|
||||
items={availableAuthModels}
|
||||
keySelector={model => model.id}
|
||||
valueSelector={model => model.displayName}
|
||||
titleSelector={model => model.description}
|
||||
searchable={availableAuthModels.length > 10}
|
||||
readOnly={readonly || readonlyAuthModelId}
|
||||
disabled={disabled}
|
||||
tiny
|
||||
fill
|
||||
onSelect={onAuthModelChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -15,11 +15,11 @@ import { ENotificationType } from '@cloudbeaver/core-events';
|
||||
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
|
||||
import { BASE_TAB_STYLES, TabList, TabPanelList, TabsState, UNDERLINE_TAB_BIG_STYLES, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { ConnectionFormActionsContext, IConnectionFormActionsContext } from './ConnectFormActionsContext';
|
||||
import connectionFormStyles from './ConnectionForm.m.css';
|
||||
import { ConnectionFormService } from './ConnectionFormService';
|
||||
import { connectionConfigContext } from './Contexts/connectionConfigContext';
|
||||
import type { IConnectionFormState } from './IConnectionFormProps';
|
||||
import connectionFormStyles from './ConnectionForm.m.css';
|
||||
import { ConnectionFormActionsContext, IConnectionFormActionsContext } from './ConnectFormActionsContext';
|
||||
|
||||
const tabsStyles = css`
|
||||
TabList {
|
||||
@@ -108,7 +108,7 @@ export const ConnectionForm = observer<ConnectionFormProps>(function ConnectionF
|
||||
const style = [BASE_TAB_STYLES, tabsStyles, UNDERLINE_TAB_STYLES, UNDERLINE_TAB_BIG_STYLES];
|
||||
const styles = useStyles(style, topBarStyles, formStyles);
|
||||
const service = useService(ConnectionFormService);
|
||||
|
||||
|
||||
const form = useForm({
|
||||
onSubmit: event => {
|
||||
if (event?.type === 'test') {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useContext } from 'react';
|
||||
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
|
||||
import { Button, getComputed, PlaceholderComponent, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import { useAuthenticationAction } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { ConnectionFormActionsContext } from './ConnectFormActionsContext';
|
||||
@@ -30,6 +31,7 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
|
||||
const driverMap = useResource(ConnectionFormBaseActions, DBDriverResource, state.config.driverId || null);
|
||||
|
||||
const driver = driverMap.data;
|
||||
const serverConfigResource = useResource(ConnectionFormBaseActions, ServerConfigResource, undefined);
|
||||
const { data: authModel } = useResource(
|
||||
ConnectionFormBaseActions,
|
||||
DatabaseAuthModelsResource,
|
||||
@@ -40,6 +42,7 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
|
||||
});
|
||||
|
||||
const authorized = authentication.providerId === AUTH_PROVIDER_LOCAL_ID || authentication.authorized;
|
||||
const disableTest = serverConfigResource.data?.distributed && !!state.config.sharedCredentials;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -48,9 +51,11 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" disabled={state.disabled || !authorized} mod={['outlined']} loader onClick={actions.test}>
|
||||
{translate('connections_connection_test')}
|
||||
</Button>
|
||||
{!disableTest && (
|
||||
<Button type="button" disabled={state.disabled || !authorized} mod={['outlined']} loader onClick={actions.test}>
|
||||
{translate('connections_connection_test')}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" disabled={state.disabled || state.readonly} mod={['unelevated']} loader onClick={actions.save}>
|
||||
{translate(state.mode === 'edit' ? 'ui_processing_save' : 'ui_processing_create')}
|
||||
</Button>
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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<IConnectionFormProps> = 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<IConnectionFormProps> = 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<IConnectionFormProps> = 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<IConnectionFormProps> = 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<IConnectionFormProps> = observe
|
||||
properties = info.authProperties;
|
||||
}
|
||||
|
||||
const sharedCredentials = config.sharedCredentials && serverConfigResource.data?.distributed;
|
||||
|
||||
function openCredentialsTab(event: React.MouseEvent<HTMLAnchorElement>) {
|
||||
event.preventDefault();
|
||||
tabsState?.open(CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form ref={formRef} className={s(style, { form: true })} disabled={driverMap.isLoading()} onChange={handleFormChange}>
|
||||
<ColoredContainer wrap overflow parent gap>
|
||||
@@ -285,63 +282,56 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
{!driver?.anonymousAccess && (authentication.authorized || !edit) && (
|
||||
<Group form gap>
|
||||
<GroupTitle>{translate('connections_connection_edit_authentication')}</GroupTitle>
|
||||
{availableAuthModels.length > 1 && (
|
||||
<Combobox
|
||||
name="authModelId"
|
||||
{serverConfigResource.resource.distributed && connectionOptionsTabService.isProjectShared(state) && (
|
||||
<FieldCheckbox
|
||||
id={config.connectionId + 'isShared'}
|
||||
name="sharedCredentials"
|
||||
title={translate('connections_connection_share_credentials_tooltip')}
|
||||
state={config}
|
||||
items={availableAuthModels}
|
||||
keySelector={model => 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')}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
{authModel && properties && (
|
||||
<ConnectionAuthModelSelector
|
||||
authModelCredentialsState={config}
|
||||
applicableAuthModels={applicableAuthModels}
|
||||
readonlyAuthModelId={!originLocal}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
onAuthModelChange={handleAuthModelSelect}
|
||||
/>
|
||||
{!sharedCredentials ? (
|
||||
<>
|
||||
<Container wrap gap hideEmpty>
|
||||
<ObjectPropertyInfoForm
|
||||
autofillToken={isSafari ? 'section-connection-authentication section-options' : 'new-password'}
|
||||
{properties && (
|
||||
<ConnectionAuthModelCredentialsForm
|
||||
credentials={config.credentials}
|
||||
properties={properties}
|
||||
state={config.credentials}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
showRememberTip
|
||||
hideEmptyPlaceholder
|
||||
tiny
|
||||
/>
|
||||
</Container>
|
||||
{credentialsSavingEnabled && !config.template && (
|
||||
<Container wrap gap>
|
||||
<FieldCheckbox
|
||||
id={config.connectionId + 'authNeeded'}
|
||||
name="saveCredentials"
|
||||
state={config}
|
||||
disabled={disabled || readonly || config.sharedCredentials}
|
||||
mod={['primary']}
|
||||
keepSize
|
||||
>
|
||||
{translate('connections_connection_edit_save_credentials')}
|
||||
</FieldCheckbox>
|
||||
{serverConfigResource.resource.distributed && connectionOptionsTabService.isProjectShared(state) && (
|
||||
<FieldCheckbox
|
||||
id={config.connectionId + 'isShared'}
|
||||
name="sharedCredentials"
|
||||
title={translate('connections_connection_share_credentials_tooltip')}
|
||||
state={config}
|
||||
disabled={disabled || readonly}
|
||||
keepSize
|
||||
>
|
||||
{translate('connections_connection_share_credentials')}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<FormFieldDescription>
|
||||
{translate('plugin_connections_connection_form_shared_credentials_manage_info')}
|
||||
<Link inline onClick={openCredentialsTab}>
|
||||
{translate('plugin_connections_connection_form_shared_credentials_manage_info_tab_link')}
|
||||
</Link>
|
||||
</FormFieldDescription>
|
||||
)}
|
||||
{!sharedCredentials && authModel && credentialsSavingEnabled && !config.template && (
|
||||
<FieldCheckbox
|
||||
id={config.connectionId + 'authNeeded'}
|
||||
name="saveCredentials"
|
||||
state={config}
|
||||
disabled={disabled || readonly || config.sharedCredentials}
|
||||
mod={['primary']}
|
||||
keepSize
|
||||
>
|
||||
{translate('connections_connection_edit_save_credentials')}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -147,13 +147,8 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
|
||||
</InputField>
|
||||
{keyAuth && <SSHKeyUploader state={handlerState} saved={keySaved} disabled={disabled || !enabled} readonly={readonly} />}
|
||||
</Container>
|
||||
{credentialsSavingEnabled && !formState.config.template && (
|
||||
<FieldCheckbox
|
||||
id={SSH_TUNNEL_ID + '_savePassword'}
|
||||
name="savePassword"
|
||||
state={handlerState}
|
||||
disabled={disabled || !enabled || readonly || formState.config.sharedCredentials}
|
||||
>
|
||||
{credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && (
|
||||
<FieldCheckbox id={SSH_TUNNEL_ID + '_savePassword'} name="savePassword" state={handlerState} disabled={disabled || !enabled || readonly}>
|
||||
{translate('connections_connection_edit_save_credentials')}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export const SSL: TabContainerPanelComponent<Props> = observer(function SSL({ st
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{credentialsSavingEnabled && !formState.config.template && (
|
||||
{credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && (
|
||||
<FieldCheckbox
|
||||
id={handler.id + '_savePassword'}
|
||||
name="savePassword"
|
||||
|
||||
+1
-4
@@ -6,7 +6,4 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export interface IConnectionInfoParams {
|
||||
projectId: string;
|
||||
connectionId: string;
|
||||
}
|
||||
export const CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID = 'shared_credentials';
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
import { ACTION_DELETE, ActionService, DATA_CONTEXT_MENU, DATA_CONTEXT_MENU_NESTED, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view';
|
||||
import { MENU_APP_ACTIONS } from '@cloudbeaver/plugin-top-app-bar';
|
||||
|
||||
import { ConnectionAuthService } from '../ConnectionAuthService';
|
||||
import { PluginConnectionsSettingsService } from '../PluginConnectionsSettingsService';
|
||||
import { PublicConnectionFormService } from '../PublicConnectionForm/PublicConnectionFormService';
|
||||
import { ACTION_CONNECTION_CHANGE_CREDENTIALS } from './Actions/ACTION_CONNECTION_CHANGE_CREDENTIALS';
|
||||
@@ -54,7 +53,6 @@ export class ConnectionMenuBootstrap extends Bootstrap {
|
||||
private readonly connectionsSettingsService: ConnectionsSettingsService,
|
||||
private readonly pluginConnectionsSettingsService: PluginConnectionsSettingsService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly connectionAuthService: ConnectionAuthService,
|
||||
private readonly serverConfigResource: ServerConfigResource,
|
||||
) {
|
||||
super();
|
||||
@@ -192,7 +190,7 @@ export class ConnectionMenuBootstrap extends Bootstrap {
|
||||
}
|
||||
|
||||
if (action === ACTION_CONNECTION_CHANGE_CREDENTIALS) {
|
||||
return this.serverConfigResource.distributed;
|
||||
return this.serverConfigResource.distributed && !connection.sharedCredentials;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -245,7 +243,7 @@ export class ConnectionMenuBootstrap extends Bootstrap {
|
||||
break;
|
||||
}
|
||||
case ACTION_CONNECTION_CHANGE_CREDENTIALS: {
|
||||
await this.connectionAuthService.auth({ connectionId: connection.id, projectId: connection.projectId }, true);
|
||||
await this.connectionsManagerService.requireConnection({ connectionId: connection.id, projectId: connection.projectId }, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,29 +7,12 @@
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import {
|
||||
CommonDialogBody,
|
||||
CommonDialogFooter,
|
||||
CommonDialogHeader,
|
||||
CommonDialogWrapper,
|
||||
ErrorMessage,
|
||||
Form,
|
||||
Loader,
|
||||
s,
|
||||
useAdministrationSettings,
|
||||
useAutoLoad,
|
||||
useErrorDetails,
|
||||
useFocus,
|
||||
useS,
|
||||
useTranslate,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import type { IConnectionInfoParams } from '@cloudbeaver/core-connections';
|
||||
import { CommonDialogHeader, CommonDialogWrapper, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionInfoResource, DBDriverResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections';
|
||||
import type { DialogComponent } from '@cloudbeaver/core-dialogs';
|
||||
|
||||
import { ConnectionAuthenticationFormLoader } from '../ConnectionAuthentication/ConnectionAuthenticationFormLoader';
|
||||
import style from './DatabaseAuthDialog.m.css';
|
||||
import { DBAuthDialogFooter } from './DBAuthDialogFooter';
|
||||
import { useDatabaseAuthDialog } from './useDatabaseAuthDialog';
|
||||
import { DatabaseCredentialsAuthDialog } from './DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog';
|
||||
import { DatabaseSecretAuthDialog } from './DatabaseSecretAuthDialog/DatabaseSecretAuthDialog';
|
||||
|
||||
interface Payload {
|
||||
connection: IConnectionInfoParams;
|
||||
@@ -38,53 +21,38 @@ interface Payload {
|
||||
}
|
||||
|
||||
export const DatabaseAuthDialog: DialogComponent<Payload> = 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<HTMLFormElement>({ 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 (
|
||||
<CommonDialogWrapper size="large">
|
||||
<CommonDialogWrapper size="large" fixedSize={useSharedCredentials}>
|
||||
<CommonDialogHeader
|
||||
title="connections_database_authentication"
|
||||
subTitle={dialog.connection?.name}
|
||||
icon={dialog.driver?.icon}
|
||||
subTitle={subtitle}
|
||||
icon={driverLoader.data?.icon}
|
||||
onReject={options?.persistent ? undefined : rejectDialog}
|
||||
/>
|
||||
<CommonDialogBody>
|
||||
<Form ref={focusedRef} className={s(styles, { submittingForm: true })} onSubmit={dialog.login}>
|
||||
<Loader state={dialog} inlineException>
|
||||
<ConnectionAuthenticationFormLoader
|
||||
config={dialog.config}
|
||||
authModelId={dialog.authModelId}
|
||||
authProperties={dialog.connection?.authProperties}
|
||||
networkHandlers={payload.networkHandlers}
|
||||
formId={`${payload.connection.projectId}:${payload.connection.connectionId}`}
|
||||
allowSaveCredentials={credentialsSavingEnabled}
|
||||
className={s(styles, { connectionAuthenticationFormLoader: true })}
|
||||
disabled={dialog.authenticating}
|
||||
hideFeatures={['nonSecuredProperty']}
|
||||
/>
|
||||
</Loader>
|
||||
</Form>
|
||||
</CommonDialogBody>
|
||||
<CommonDialogFooter>
|
||||
<DBAuthDialogFooter isAuthenticating={dialog.authenticating} onLogin={dialog.login}>
|
||||
{dialog.authException && (
|
||||
<ErrorMessage
|
||||
text={errorDetails.message ?? translate('core_blocks_exception_message_error_message')}
|
||||
hasDetails={errorDetails.hasDetails}
|
||||
className={style.errorMessage}
|
||||
onShowDetails={errorDetails.open}
|
||||
/>
|
||||
)}
|
||||
</DBAuthDialogFooter>
|
||||
</CommonDialogFooter>
|
||||
{useSharedCredentials ? (
|
||||
<DatabaseSecretAuthDialog connectionKey={payload.connection} onLogin={resolveDialog} />
|
||||
) : (
|
||||
<DatabaseCredentialsAuthDialog
|
||||
connection={payload.connection}
|
||||
networkHandlers={payload.networkHandlers}
|
||||
resetCredentials={payload.resetCredentials}
|
||||
onLogin={resolveDialog}
|
||||
/>
|
||||
)}
|
||||
</CommonDialogWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
+87
@@ -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<Props>(function DatabaseCredentialsAuthDialog({
|
||||
connection,
|
||||
networkHandlers,
|
||||
resetCredentials,
|
||||
onLogin,
|
||||
}) {
|
||||
const styles = useS(style);
|
||||
const translate = useTranslate();
|
||||
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
|
||||
|
||||
const { credentialsSavingEnabled } = useAdministrationSettings();
|
||||
const dialog = useDatabaseCredentialsAuthDialog(connection, networkHandlers, resetCredentials, onLogin);
|
||||
const errorDetails = useErrorDetails(dialog.authException);
|
||||
|
||||
useAutoLoad(DatabaseCredentialsAuthDialog, dialog);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommonDialogBody>
|
||||
<Form ref={focusedRef} className={s(styles, { submittingForm: true })} onSubmit={dialog.login}>
|
||||
<Loader state={dialog} inlineException>
|
||||
<ConnectionAuthenticationFormLoader
|
||||
config={dialog.config}
|
||||
authModelId={dialog.authModelId}
|
||||
authProperties={dialog.connection?.authProperties}
|
||||
networkHandlers={networkHandlers}
|
||||
formId={`${connection.projectId}:${connection.connectionId}`}
|
||||
allowSaveCredentials={credentialsSavingEnabled}
|
||||
className={s(styles, { connectionAuthenticationFormLoader: true })}
|
||||
disabled={dialog.authenticating}
|
||||
hideFeatures={['nonSecuredProperty']}
|
||||
/>
|
||||
</Loader>
|
||||
</Form>
|
||||
</CommonDialogBody>
|
||||
<CommonDialogFooter>
|
||||
<DatabaseCredentialsAuthDialogFooter isAuthenticating={dialog.authenticating} onLogin={dialog.login}>
|
||||
{dialog.authException && (
|
||||
<ErrorMessage
|
||||
text={errorDetails.message ?? translate('core_blocks_exception_message_error_message')}
|
||||
hasDetails={errorDetails.hasDetails}
|
||||
className={style.errorMessage}
|
||||
onShowDetails={errorDetails.open}
|
||||
/>
|
||||
)}
|
||||
</DatabaseCredentialsAuthDialogFooter>
|
||||
</CommonDialogFooter>
|
||||
</>
|
||||
);
|
||||
});
|
||||
+1
-1
@@ -32,7 +32,7 @@ export interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DBAuthDialogFooter = observer<React.PropsWithChildren<Props>>(function DBAuthDialogFooter({
|
||||
export const DatabaseCredentialsAuthDialogFooter = observer<React.PropsWithChildren<Props>>(function DatabaseCredentialsAuthDialogFooter({
|
||||
isAuthenticating,
|
||||
onLogin,
|
||||
className,
|
||||
+7
-2
@@ -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);
|
||||
|
||||
+88
@@ -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<Props>(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 <Loader loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommonDialogBody noBodyPadding>
|
||||
<ItemList>
|
||||
{secrets.map(secret => (
|
||||
<ListItem key={secret.secretId} onClick={() => handleSecretSelect(secret.secretId)}>
|
||||
<ListItemName>{secret.displayName}</ListItemName>
|
||||
</ListItem>
|
||||
))}
|
||||
</ItemList>
|
||||
</CommonDialogBody>
|
||||
{state.exception && (
|
||||
<CommonDialogFooter>
|
||||
<Group secondary vertical dense>
|
||||
<ExceptionMessage exception={state.exception} inline />
|
||||
</Group>
|
||||
</CommonDialogFooter>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
+6
-6
@@ -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');
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user